Last week I had to implement the new React Context API for a React 15 project. Migrating to React 16 was not the option due to a big codebase so I headed to React sources for references.
The first thing I noted was the second argument of the createContext
function:
export function createContext<T>(
defaultValue: T,
calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {
The second argument is not mentioned in the React docs so started discovering what that is. After some investigation I found out that there's an optimization that can be applied to React Context.
So what does it actually do?
React Context allows its consumers to observe certain bits of a bitmask produced by the calculateChangedBits
function that can be passed as the second argument to createContext
. If one of the observed bits changes, a context consumer gets re-rendered. If not, it's not going to do an unneeded re-render. Sounds great! Let's see how it works in practice.
Before we start
If you're not familiar with bitwise operators, check out this MDN page.
A Sample App
I created a simple Ice Cream Constructor app that has two selects and shows a list of available options based on selected values. The filter is a simple React Context that holds the state of selected values and provides an API for its consumers to get a current filter state and update it. You can check out the full demo here.
First of all, let's define an object that's going to map context consumers to bits they observe:
export default {
fruit: 0b01,
topping: 0b10,
};
0b
is a binary prefix meaning that a number following after it is binary. By putting 1s and 0s we tell what bits are going to be observed. There won't be any observed bits if we put 0, and every bit is observed if we put all 1s. In our example we say that fruit is going to observe the first bit and topping is going to observe the second bit.
calculateChangedBits
Now Let's create a filter context:
import React from 'react';
import observedBitsMap from './observedBitsMap';
const calculateChangedBits = (currentFilter, nextFilter) => {
let result = 0;
Object.entries(nextFilter.filter).forEach(([key, value]) => {
if (value !== currentFilter.filter[key]) {
result = result | observedBitsMap[key];
}
});
return result;
};
const initialValue = {
filter: {
fruit: 'banana',
topping: 'caramel',
},
};
export const FilterContext = React.createContext(initialValue, calculateChangedBits);
calculateChangedBits
is passed as the second argument to React.createContext
. It takes current context value and new context value and returns a value that represents changed context values that are changed.
unstable_observedBits
While result of calling calculateChangedBits
represents the whole change, unstable_observedBits
tells what particular bits of the whole change are going to trigger a context consumer update. It's passed as the second argument to React.useContext
:
import React from 'react';
import observedBitsMap from './observedBitsMap';
import { FilterContext } from './FilterContext';
const FilterItem = ({ name, children }) => {
const context = React.useContext(FilterContext, observedBitsMap[name]);
const onChange = React.useCallback(
(e) => {
context.onFilterChange(e);
},
[context.onFilterChange],
);
return children({ name, onChange, value: context.filter[name] });
}
If you want to use a regular JSX Context.Consumer
you can pass unstable_observedBits
as a prop:
<FilterContext.Consumer unstable_observedBits={observedBitsMap[name]}>
...
If unstable_observedBits
is passed, consumer is going to be updated only if the result of bitwise AND
on what we got from calculateChangedBits
's execution and unstable_observedBits
is not equal to 0.
Limitations
As you can see from the unstable_observedBits
name, this is an unstable experimental feature. Every time a context value changes React shows a warning:
Also, there's a limitation on the number of bits that can be observed. It is restricted by the max integer size in V8 for 32-bit systems. This means that we can't effectively re-render observe more than 30 different consumers.
Conclusion
Even though React Context API provides a great optimization opportunity, I don't think it should be widely used. This whole thing is more about exploring what the library hides rather than finding something for usual usage. If you think that you want to apply this optimization in your project, ask yourself "why are my renders so slow that I need to use a deep optimization?" question first.
I guess that this feature is going to be used mostly in libraries even when it turns to stable. But I'm really interested in what direction would the implementation evolve.
Top comments (3)
Oh, I didn't know about second parameter of
createContext
. As you said, it's probably not widely used, but good to know. Maybe will be useful in the future projects, who knows? Thank you for this article.Thanks! Happy to hear that it helps to learn something new
Thanks!
I saw
react-tracked
but didn’t get a chance to use it yet. Also I didn’t know that it usescalculateChangedBits
. Great real-world example for me and those who interested!