The problem we are solving
The context in react can contain many values ββand different consumers of the context can use only part of the values. However, when any value changes from the context, all consumers (in particular, all components that use useContext
) will be rerendered , even if they do not depend on the changed part of the data. The problem is quite discussed and has many different solutions. Here are some of them. I created this example to demonstrate the problem. Just open the console and press the buttons.
purpose
Our solution should change the existing codebases to a minimum. I want to create my own custom hook useSmartContext
with the same signature as that of useContext
, but which will only re-render the component when the used part of the context changes.
Idea
Find out what is being used by the component by wrapping the return useSmartContext
value in a Proxy.
Implementation
Step 1.
We create our own hook.
const useSmartContext(context) {
const usedFieldsRef = useRef(new Set());
const proxyRef = useRef(
new Proxy(
{},
{
get(target, prop) {
usedPropsRef.current.add(prop);
return context._currentValue[prop];
}
}
)
);
return proxyRef.current;
}
We have created a list in which we will store the used context fields. We created a proxy with a get
trap in which we fill this list. Target
it doesn't matter to us, so I passed an empty object as the first argument {}
.
Step 2.
You need to get the value of the context when it is updated and compare the value of the fields from the list usedPropsRef
with the previous values. If something has changed, then trigger a re-rendering. useContext
We cannot use it inside our hook, otherwise our hook will also start causing re-rendering for all changes. Here dances with a tambourine begin. I originally hoped to subscribe to context changes with context.Consumer
. Namely like this:
React.createElement(context.Consumer, {}, (newContextVakue) => {/* handle */})
. . - , , , .
React
, useContext
. , , , . - . _currentValue
. , undefined
. ! Proxy , . Object.defineProperty
.
let val = context._currentValue;
let notEmptyVal = context._currentValue;
Object.defineProperty(context, "_currentValue", {
get() {
return val;
},
set(newVal) {
if (newVal) {
// !
}
val = newVal;
}
});
! : useSmartContext
Object.defineProperty
. useSmartContext
createContext
.
export const createListenableContext = () => {
const context = createContext();
const listeners = [];
let val = context._currentValue;
let notEmptyVal = context._currentValue;
Object.defineProperty(context, "_currentValue", {
get() {
return val;
},
set(newVal) {
if (newVal) {
listeners.forEach((cb) => cb(notEmptyVal, newVal));
notEmptyVal = newVal;
}
val = newVal;
}
});
context.addListener = (cb) => {
listeners.push(cb);
return () => listeners.splice(listeners.indexOf(cb), 1);
};
return context;
};
, . ,
const useSmartContext = (context) => {
const usedFieldsRef = useRef(new Set());
useEffect(() => {
const clear = context.addListener((prevValue, newValue) => {
let isChanged = false;
usedFieldsRef.current.forEach((usedProp) => {
if (!prevValue || newValue[usedProp] !== prevValue[usedProp]) {
isChanged = true;
}
});
if (isChanged) {
//
}
});
return clear;
}, [context]);
const proxyRef = useRef(
new Proxy(
{},
{
get(target, prop) {
usedFieldsRef.current.add(prop);
return context._currentValue[prop];
}
}
)
);
return proxyRef.current;
};
3.
. useState
, . , . - ?
// ...
const [, rerender] = useState();
const renderTriggerRef = useRef(true);
// ...
if (isChanged) {
renderTriggerRef.current = !renderTriggerRef.current;
rerender(renderTriggerRef.current);
}
, . . useContext
->useSmartContext
createContext
->createListenableContext
.
, !
, . .
While writing this article, I came across another library that solves the same problem with optimizing redraws when using context. The solution of this library, in my opinion, is the most correct one I have seen. Its sources are much more readable and they gave me a couple of ideas on how to make our example production ready without changing the way it is used. If the meeting received a positive response from you, then I will write about the new implementation.
Thank you all for your attention.