React useContextSelector hook in userland
use-context-selector
React Context and useContext is often used to avoid prop drilling, however it's known that there's a performance issue. When a context value is changed, all components that useContext will re-render.
useContextSelector is recently proposed. While waiting for the process, this library provides the API in userland.
Install
npm install use-context-selector
Usage
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { createContext, useContextSelector } from 'use-context-selector';
const context = createContext(null);
const Counter1 = () => {
const count1 = useContextSelector(context, v => v[0].count1);
const setState = useContextSelector(context, v => v[1]);
const increment = () => setState(s => ({
...s,
count1: s.count1 + 1,
}));
return (
<div>
<span>Count1: {count1}</span>
<button type="button" onClick={increment}>+1</button>
{Math.random()}
</div>
);
};
const Counter2 = () => {
const count2 = useContextSelector(context, v => v[0].count2);
const setState = useContextSelector(context, v => v[1]);
const increment = () => setState(s => ({
...s,
count2: s.count2 + 1,
}));
return (
<div>
<span>Count2: {count2}</span>
<button type="button" onClick={increment}>+1</button>
{Math.random()}
</div>
);
};
const StateProvider = ({ children }) => {
const [state, setState] = useState({ count1: 0, count2: 0 });
return (
<context.Provider value={[state, setState]}>
{children}
</context.Provider>
);
};
const App = () => (
<StateProvider>
<Counter1 />
<Counter2 />
</StateProvider>
);
ReactDOM.render(<App />, document.getElementById('app'));
Technical memo
React context by nature triggers propagation of component re-rendering
if a value is changed. To avoid this, this library uses undocumented
feature of calculateChangedBits
. It then uses a subscription model
to force update when a component needs to re-render.
API
createContext
This creates a special context for useContextSelector
.
Parameters
defaultValue
any
Examples
const PersonContext = createContext({ firstName: '', familyName: '' });
Returns React.Context
useContextSelector
This hook returns context selected value by selector.
It will only accept context created by createContext
.
It will trigger re-render if only the selected value is referencially changed.
Parameters
context
React.Contextselector
Function
Examples
const firstName = useContextSelector(PersonContext, state => state.firstName);
Returns any
useContext
This hook returns the entire context value.
Use this instead of React.useContext for consistent behavior.
Parameters
context
React.Context
Examples
const person = useContext(PersonContext);
Returns any
Limitations
- Subscriptions are per-context basis. So, even if there are multiple context providers in a component tree, all components are subscribed to all providers. This may lead false positives (extra re-renders).
- In order to stop propagation,
children
of a context provider has to be either created outside of the provider or memoized withReact.memo
. - Context consumers are not supported.
- The stale props issue can't be solved in userland. (workaround with try-catch)
Examples
The examples folder contains working examples.
You can run one of them with
PORT=8080 npm run examples:minimal
and open http://localhost:8080 in your web browser.