Components that call useContext
directly or indirectly through custom hook will re render every time value of the provider changes.
You can call useContext
from a container and let the container render a pure component or have the container call a functional component that is memoized with useMemo.
In the example below count is changed every 100 milliseconds but because useMyContext
returns Math.floor(count / 10)
the components will only render once every second. The containers will "render" every 100 milliseconds but they will return the same jsx 9 out of 10 times.
const {
useEffect,
useMemo,
useState,
useContext,
memo,
} = React;
const MyContext = React.createContext({ count: 0 });
function useMyContext() {
const { count } = useContext(MyContext);
return Math.floor(count / 10);
}
// simple context that increments a timer
const Context = ({ children }) => {
const [count, setCount] = useState(0);
useEffect(() => {
const i = setInterval(() => {
setCount((c) => c + 1);
}, [100]);
return () => clearInterval(i);
}, []);
const value = useMemo(() => ({ count }), [count]);
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
};
//pure component
const MemoComponent = memo(function Component({ count }) {
console.log('in memo', count);
return <div>MemoComponent {count}</div>;
});
//functional component
function FunctionComponent({ count }) {
console.log('in function', count);
return <div>Function Component {count}</div>;
}
// container that will run every time context changes
const ComponentContainer1 = () => {
const count = useMyContext();
return <MemoComponent count={count} />;
};
// second container that will run every time context changes
const ComponentContainer2 = () => {
const count = useMyContext();
//using useMemo to not re render functional component
return useMemo(() => FunctionComponent({ count }), [
count,
]);
};
function App() {
console.log('App rendered only once');
return (
<Context>
<ComponentContainer1 />
<ComponentContainer2 />
</Context>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
With React-redux useSelector a component that calls useSelector
will only be re rendered when the function passed to useSelector returns something different than the last time it was run and all functions passed to useSelector will run when redux store changes. So here you don't have to split container and presentation to prevent re render.
Components will also re render if their parent re renders and passes them different props or if parent re renders and the component is a functional component (functional components always re render). But since in the code example the parent (App) never re renders we can define the containers as functional components (no need to wrap in React.memo).
The following is an example of how you can create a connected component similar to react redux connect but instead it'll connect to context:
const {
useMemo,
useState,
useContext,
useRef,
useCallback,
} = React;
const { createSelector } = Reselect;
const MyContext = React.createContext({ count: 0 });
//react-redux connect like function to connect component to context
const connect = (context) => (mapContextToProps) => {
const select = (selector, state, props) => {
if (selector.current !== mapContextToProps) {
return selector.current(state, props);
}
const result = mapContextToProps(state, props);
if (typeof result === 'function') {
selector.current = result;
return select(selector, state, props);
}
return result;
};
return (Component) => (props) => {
const selector = useRef(mapContextToProps);
const contextValue = useContext(context);
const contextProps = select(
selector,
contextValue,
props
);
return useMemo(() => {
const combinedProps = {
...props,
...contextProps,
};
return (
<React.Fragment>
<Component {...combinedProps} />
</React.Fragment>
);
}, [contextProps, props]);
};
};
//connect function that connects to MyContext
const connectMyContext = connect(MyContext);
// simple context that increments a timer
const Context = ({ children }) => {
const [count, setCount] = useState(0);
const increment = useCallback(
() => setCount((c) => c + 1),
[]
);
return (
<MyContext.Provider value={{ count, increment }}>
{children}
</MyContext.Provider>
);
};
//functional component
function FunctionComponent({ count }) {
console.log('in function', count);
return <div>Function Component {count}</div>;
}
//selectors
const selectCount = (state) => state.count;
const selectIncrement = (state) => state.increment;
const selectCountDiveded = createSelector(
selectCount,
(_, divisor) => divisor,
(count, { divisor }) => Math.floor(count / divisor)
);
const createSelectConnectedContext = () =>
createSelector(selectCountDiveded, (count) => ({
count,
}));
//connected component
const ConnectedComponent = connectMyContext(
createSelectConnectedContext
)(FunctionComponent);
//app is also connected but won't re render when count changes
// it only gets increment and that never changes
const App = connectMyContext(
createSelector(selectIncrement, (increment) => ({
increment,
}))
)(function App({ increment }) {
const [divisor, setDivisor] = useState(0.5);
return (
<div>
<button onClick={increment}>increment</button>
<button onClick={() => setDivisor((d) => d * 2)}>
double divisor
</button>
<ConnectedComponent divisor={divisor} />
<ConnectedComponent divisor={divisor * 2} />
<ConnectedComponent divisor={divisor * 4} />
</div>
);
});
ReactDOM.render(
<Context>
<App />
</Context>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
memo
would be the HOC to help try to cut down on extraneous renders (though by definition they aren't extraneous at all as they render exactly by design!!) – Critchfield