TL&DR:
This is an experimental attempt to show that Context can be used for State Management, and is used by libraries.Also, you can have a look around THIS issue on GitHub
What is Context?
The Context was React's answer to "props drilling", a mechanism to share data between multiple child components through a common parent component.
Context is like Refs, but it comes with providers. It means, every Context has it's own provider component, and the shared value is passed through the props of that component.
const AppContext = React.createContext();
function SomeComponent() {
return (
<AppContext.Provider value={initialValue}>
<ChildComponentOne />
<ClildComponentTwo />
</AppContext.Provider>
)
}
Context for State Management?
If you are already into React, then you also know not to use Context directly. That's because the shared value is passed through the props of the provider component. So, when the reference to that shared value changes, the parent component always triggers a re-renders from the provided component. This is visible if the profile the Context example from React's documentation.
I re-created the example, then profiled it by enabling highlight on re-render of the component. The App consists of four components - two components only trigger increment, the other two only shows the values. You can find my code HERE. We can see bellow that all components are re-rendering on every single state change, along with the main app component.
Then why Context?
Given this behavior, it might seem impractical to use Context. But if you dig inside the state management libraries for React, you will see that they use Context underneath (namely MobX. So what's the difference?
How we pass the value through the provider makes all the difference. We pass the value through the Provider's props. So, if the reference of that value changes, it triggers re-render. So, if we want to stop that unnecessary re-render, we have to update values without changing the reference.
Start the experiment already!
Let's start with a class that will be used as the primitive to store data.
// TypeScript
type Callback = {
id: string,
cb: () => void,
};
class ReactiveVariable<T> {
private value: T | undefined;
private reactions: Callback[] = [];
setValue(v: T): void {
this.value = v;
this.reactions.forEach(r => r.cb());
}
getValue(): T | undefined {
return this.value;
}
addReaction(cb: Callback['cb']): string {
const id: string = `${Math.random() * 1000}-${Math.random() * 1000}-${Math.random() * 1000}`;
this.reactions.push({ id, cb });
return id;
}
removeReaction(id: string): void {
this.reactions = this.reactions.filter(r => r.id !== id);
}
}
This is a generic class that can store any type of data. The difference is that it can keep a list of the callback functions that will be executed if the stored value changes.
Now, let's create our state.
// TypeScript
class ReactiveStateClass {
inc1: ReactiveVariable<number> = new ReactiveVariable();
inc2: ReactiveVariable<number> = new ReactiveVariable();
increment1(): void {
const currentValue = this.inc1.getValue() ?? 0;
this.inc1.setValue(currentValue + 1);
}
increment2(): void {
const currentValue = this.inc2.getValue() ?? 0;
this.inc2.setValue(currentValue + 1);
}
}
export const ReactiveState = new ReactiveStateClass();
Now we have two variables that store two numbers in our state. We can call increment1()
and increment2()
function to increment those two numbers.
Let's create our Context.
// Context
const IncrementContext = React.createContext(ReactiveState);
To keep the components clean, we can write hooks that will connect to the Context and apply reaction when the value changes. We can expose the updated value through React.useState() to trigger re-render when the value changes.
// TypeScript
function useInc1(): number | undefined {
const [value, setValue] = React.useState<number>();
const context = React.useContext(IncrementContext);
React.useEffect(() => {
const id = context.inc1.addReaction(() => setValue(context.inc1.getValue()));
return () => context.inc1.removeReaction(id);
});
return value;
}
function useInc2(): number | undefined {
const [value, setValue] = React.useState<number>();
const context = React.useContext(IncrementContext);
React.useEffect(() => {
const id = context.inc2.addReaction(() => setValue(context.inc2.getValue()));
return () => context.inc2.removeReaction(id);
});
return value;
}
Now, let's connect the Context with our application.
// TypeScript
// Render value
function IncrementOneView() {
const inc1 = useInc1();
return (
<div>
Increment One : {inc1}
</div>
);
}
// Render value
function IncrementTwoView() {
const inc2 = useInc2();
return (
<div>
Increment Two : {inc2}
</div>
);
}
// Trigger increment
function IncrementOneButton() {
const context = React.useContext(IncrementContext);
return (
<div>
<button
onClick={() => context.increment1()}
>
Increment One
</button>
</div>
)
}
// Trigger increment
function IncrementTwoButton() {
const context = React.useContext(IncrementContext);
return (
<div>
<button
onClick={() => context.increment2()}
>
Increment Two
</button>
</div>
)
}
// Our main application
function App() {
return (
<IncrementContext.Provider value={ReactiveState}>
<div style={ViewStyle}>
<IncrementOneView />
<IncrementTwoView />
<br />
<IncrementOneButton />
<IncrementTwoButton />
</div>
</IncrementContext.Provider>
);
}
Now that everything is set up, let's profile it with the Dev Tools.
As we can see, we are re-rendering only the child that needs to be re-rendered!
You can find the source code HERE if you want to have a look at it.
Top comments (0)