Push or Pull State with React Context (useContext, useState, useRef, useCallback)
tldr: this CodeSandbox demonstrates a way to fetch state via a context only when required, eliminating unnecessary renders.
Overview
The React Context API has made accessing state anywhere in the component tree (and writing clean code without prop drilling) easier than ever before.
This article:
- examines a typical pattern for sharing state using the Context API, and
- discusses performance implications of the above pattern, and proposes a simple modification using the
useRef
anduseCallback
hooks to eliminates unnecessary renders.
The basic pattern
The basic pattern for making some value
and the corresponding setValue
function globally available is based around the below ValueProvider
:
const ValueContext = createContext<number | null>(null);
const SetValueContext = createContext<Dispatch<SetStateAction<number>> | null>(
null
);
const ValueProvider = ({ children }: { children: React.ReactNode }) => {
const INITIAL_VALUE = 0;
const [value, setValue] = useState<number>(INITIAL_VALUE);
return (
<ValueContext.Provider value={value}>
<SetValueContext.Provider value={setValue}>
{children}
</SetValueContext.Provider>
</ValueContext.Provider>
);
};
For the first usage example, we define 3 components that consume the context:
SetValueButton
: setsvalue
to a random number when clicked,LogValueButton
: logs the currentvalue
in the console when clicked, andValueDisplay
: displays the currentvalue
.
const SetValueButton = () => {
const setValue = useContext(SetValueContext);
return (
<ButtonWithRenderCount
text="set random value"
onClick={() => setValue && setValue(Math.round(Math.random() * 100))}
/>
);
};
const LogValueButton = () => {
const value = useContext(ValueContext);
return (
<ButtonWithRenderCount
text="console.log(value)"
onClick={() => console.log(value)}
/>
);
};
const ValueDisplay = () => {
const value = useContext(ValueContext);
return <div>value: {value}</div>;
};
export const App = () => (
<ValueProvider>
<SetValueButton />
<LogValueButton />
<ValueDisplay />
</ValueProvider>
);
Note the buttons are wrapped in ButtonsWithRenderCount
, which (just for the purpose of the below demo) displays the render count adjacent the button.
const ButtonWithRenderCount = ({
text,
onClick
}: {
text: string;
onClick: () => void;
}) => {
const renderCountRef = useRef(0);
renderCountRef.current += 1;
return (
<div>
<button onClick={onClick}>{text}</button>
<span>renders: {renderCountRef.current}</span>
</div>
);
};
Demo
Test out each button in the below demo. The buttons behave as expected, but notice that the LogValueButton
component renders every time value
changes, even though there are no UI changes 🤔 :
Problems with this pattern
The React API doc for useContext sheds light on why these unwanted renders occur:
When the nearest
<MyContext.Provider>
above the component updates [i.e whensetState
is called], this Hook will trigger a rerender with the latest contextvalue
passed to thatMyContext
provider.
Performance implications
Imagine a use case where value
changes frequently (i.e tracking cursor position, the scroll position of a container, or perhaps the completion progress of a background process). Now additionally imagine there are a large number of these LogValueButtons
, say, on each line in a list of search results.
React renders are not free (even if the final result is “no changes, do not update the DOM”), so an app using this pattern in this type of scenario would suffer from performance issues.
The improved pattern
The LogValueButton
doesn’t need to always know the current value
– only where to get it when clicked. We could say the above pattern causes the LogValueButton
to “subscribe” to the state (the “Push” pattern), while a more efficient pattern would allow it to “get” the state when required (the “Pull” pattern).
To enable an option to consume value
via the “Pull” pattern, ValueProvider
can be updated as follows:
const ValueContext = createContext<number | null>(null);
+ const ValueRefContext = createContext<MutableRefObject<number> | null>(null);
const SetValueContext = createContext<Dispatch<SetStateAction<number>> | null>(
null
);
const ValueProvider = ({ children }: { children: React.ReactNode }) => {
const INITIAL_VALUE = 0;
const [value, setValue] = useState<number>(INITIAL_VALUE);
+ const valueRef = useRef<number>(INITIAL_VALUE);
+ valueRef.current = value;
return (
<ValueContext.Provider value={value}>
+ <ValueRefContext.Provider value={valueRef}>
<SetValueContext.Provider value={setValue}>
{children}
</SetValueContext.Provider>
+ </ValueRefContext.Provider>
</ValueContext.Provider>
);
};
How it works
React Docs discuss the most common use for refs (tl;dr – refs allow direct access to DOM nodes), but the useRef API Doc reveals the property that makes this hook very powerful for solving a broader class of problems:
useRef
returns a mutable ref object whose.current
property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
In other words, while the .current
property of the valueRef
changes frequently, the valueRef
object is stable for the lifetime of the provider (note that “lifetime” does not mean “a render cycle” – it means until the ValueProvider
is unmounted, in which case all child consumer components are also unmounted).
Because of this, we can define a custom hook useGetValue
, and sleep soundly trusting it returns a stable callback that returns an object with the .current
property always pointing to the latest value
:
const useGetValue = () => {
const valueRef = useContext(ValueRefContext);
return useCallback(() => valueRef?.current, [valueRef]);
};
To test out the new hook, we add an additional button, LogGetValueButton
, which behaves in exactly the same way as the LogValueButton
but only renders once 🎉🥳 :
const LogGetValueButton = () => {
const getValue = useGetValue();
return (
<ButtonWithRenderCount
text="console.log(getValue())"
onClick={() => console.log(getValue())}
/>
);
};
After dropping it in alongside the other components…
export const App = () => (
<ValueProvider>
<SetValueButton />
<LogValueButton />
+ <LogGetValueButton />
<ValueDisplay />
</ValueProvider>
);
…let’s test it out!
Notice that while the LogValueButton
continues to render every time value
changes, LogGetValueButton
only renders once.
Questions?
Do you have any questions or feedback? Feel free to leave a comment below, or to reach out to me directly.