26
loading...
This website collects cookies to deliver better user experience
use-context-selector
that allows you to avoid useless re-render.use-context-selector
exposes:createContext
: a function to create a React context (yep like the React one). You can pass an optional initial value.useContextSelector
: a hook to get data from the context. It takes as first parameter the created context, and as second parameter a selector, if an identity function is passed (i.e. v => v
), you will watch all changes of the context.useContext
: a hook to be notified of all changes made in the context (like the React one).Note: In reality, the lib exposes also: useContextUpdate
, BridgeProvider
and useBridgeValue
that I don't gonna talk about in this article.
import {
createContext,
useContextSelector,
} from "use-context-selector";
const MyContext = createContext();
function MyProvider({ children }) {
const [value, setValue] = useState("Initial value");
return (
<MyContext.Provider value={{ value, setValue }}>
{children}
</MyContext.Provider>
);
}
function ComponentUsingOnlySetter() {
const setValue = useContextSelector(
MyContext,
(state) => state.setValue
);
return (
<button
type="button"
onClick={() => setValue("Another value")}
>
Change value
</button>
);
}
function ComponentUsingOnlyValue() {
const value = useContextSelector(
MyContext,
(state) => state.value
);
return <p>The value is: {value}</p>;
}
function App() {
return (
<MyProvider>
<ComponentUsingOnlySetter />
<ComponentUsingOnlyValue />
</MyProvider>
);
}
import {
createContext,
useContextSelector,
} from "use-context-selector";
const MyContext = createContext();
const useMyContext = (selector) =>
useContextSelector(MyContext, selector);
// I just rewrite this component,
// but it will be the same for the other one
function ComponentUsingOnlyValue() {
const value = useMyContext((state) => state.value);
return <p>The value is: {value}</p>;
}
Warning: Contrary to the React API, you don't have access to a Consumer
component from the context. The Consumer
can be useful when you have class components (and not functional component), in this case I recommend you to make an HOC that will use the useContextSelector
. Or migrate to functional components :)
useState
or useReducer
).createContext
. This method will just:Consumer
component from it.Provider
by our own implementation.
import { createContext as createContextOriginal } from "react";
function createContext(defaultValue) {
// We are going to see next how to store the defaultValue
const context = createContextOriginal();
delete context.Consumer;
// Override the Provider by our own implem
// We are going next to implement the `createProvider` function
context.Provider = createProvider(context.Provider);
return context;
}
createProvider
function:import { useRef } from "react";
function createProvider(ProviderOriginal) {
return ({ value, children }) => {
// Keep the current value in a ref
const valueRef = useRef(value);
// Keep the listeners in a Set
// For those who doesn't know Set
// You can compare it to Array
// But only store unique value/reference
// And give a nice API: add, delete, ...
const listenersRef = useRef(new Set());
// We don't want the context reference to change
// So let's store it in a ref
const contextValue = useRef({
value: valueRef,
// Callback to register a listener
registerListener: (listener) => {
// Add the listener in the Set of listeners
listenersRef.current.add(listener);
// Return a callback to unregister/remove the listener
return () => listenersRef.current.delete(listener);
},
listeners: new Set(),
});
useEffect(() => {
// Each time the value change let's:
// - change the valueRef
// - notify all listeners of the new value
valueRef.current = value;
listenersRef.current.forEach((listener) => {
listener(value);
});
}, [value]);
return (
<ProviderOriginal value={contextValue.current}>
{children}
</ProviderOriginal>
);
};
}
useContextSelector
and its listener is:import { useContext, useEffect } from "react";
export default function useContextSelector(
context,
selector
) {
const { value, registerListener } = useContext(context);
// In the next part we will how to really implement this
const selectedValue = selector(value);
useEffect(() => {
const updateValueIfNeeded = (newValue) => {
// We are going to implement the logistic in the next part
};
const unregisterListener = registerListener(
updateValueIfNeeded
);
return unregisterListener;
}, [registerListener, value]);
return selectedValue;
}
updateValueIfNeeded
.useContextSelector
becomes:import {
useContext,
useEffect,
useRef,
useState,
} from "react";
export default function useContextSelector(
context,
selector
) {
const { value, registerListener } = useContext(context);
// We use a state to store the selectedValue
// It will re-render only if the value changes
// As you may notice, I lazily initialize the value
const [selectedValue, setSelectedValue] = useState(() =>
selector(value)
);
const selectorRef = useRef(selector);
useEffect(() => {
// Store the selector function at each render
// Because maybe the function has changed
selectorRef.current = selector;
});
useEffect(() => {
const updateValueIfNeeded = (newValue) => {
// Calculate the new selectedValue
const newSelectedValue =
selectorRef.current(newValue);
// Always update the value
// React will only re-render if the reference has changed
// Use the callback to be able to select callback too
// Otherwise it will the selected callback
setSelectedValue(() => newSelectedValue);
};
const unregisterListener = registerListener(
updateValueIfNeeded
);
return unregisterListener;
}, [registerListener, value]);
return selectedValue;
}
import { createContext as createContextOriginal } from "react";
function createContext(defaultValue) {
// Just put the defaultValue
// And put a noop register function
const context = createContextOriginal({
value: {
current: defaultValue,
},
register: () => {
return () => {};
}
});
delete context.Consumer;
// Override the Provider by our own implem
// We are going next to implement the `createProvider` function
context.Provider = createProvider(context.Provider);
return context;
}
use-context-selector
.react-redux
implementation for performance purposes.useContextUpdate
.