Preface
React Context is a commonly used state management mechanism provided by React to developers. Context can effectively transfer the same state in multi-level components, and can automatically notify each component to update when the status is updated. So how does React Context do this, and why does it need to be so designed?
Why do you need Context
in React's concept of data management, it has always followed the concept of individual data flow and data invariance. When we need to pass the state from the parent component to the child component, we often need to pass it explicitly through Props, for example:
const Father:FC = () => {
const [count, setCount] = useState<number>(0)
return (
<Son count={count} />
)
}
const Son:FC = (props) => {
const { count } = props;
return (
<span>{count}</span>
)
}
but what if the parent component needs to pass the state to the child component of the child component, that is, the descendant component? Or does the parent component need to be passed to multiple child components at the same time? Of course, continue to use props for layer-by-layer display transfer can certainly achieve this requirement, but that kind of code is too cumbersome and difficult to maintain, if you can maintain a global variable similar to that in Js in the parent component, wouldn't it be nice for all child components to use this global variable?
Yes, that's what Context is for, but it's much more than that.
What is
Context?
Context provides a way to pass data between component trees without manually adding props for each layer of components.
how do I use Context?
create Context
first, we need to create a React Context object in the parent component using React.createContext, which accepts an input parameter as the default value for the current Context.
import React from 'react'
const Context = React.createContext(defaultValue)
pass data down
use the Provide component returned by the Context object to wrap the subcomponents that need to pass data.
each Context object returns a Provider React component that receives a value property that passes data down to the consumer component. When the value value of Provider changes, all of its internal consumer components are re-rendered.
const Father:FC = () => {
const [count, setCount] = useState<number>(0)
return (
<Context.Provider value={count}>
<Son />
</Context.Provider>
)
}
receive data
the wrapped child component uses useContext to get the data passed by the parent component.
const Son:FC = (props) => {
const value = React.useContext(Context);
return (
<span>{value}</span>
)
}
how and why is Context implemented this way?
Let's go back to the first step in using Context and read the source code to see what kind of work createContext has done.
removes some interfering code, but what createContext does is actually very simple, creating an object, saving the value of the current context, and returning a Provide component.
import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import type {ReactProviderType} from 'shared/ReactTypes';
import type {ReactContext} from 'shared/ReactTypes';
export function createContext<T>(defaultValue: T): ReactContext<T> {
// TODO: Second argument used to be an optional `calculateChangedBits`
// function. Warn to reserve for future use?
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
// As a workaround to support multiple concurrent renderers, we categorize
// some renderers as primary and others as secondary. We only expect
// there to be two concurrent renderers at most: React Native (primary) and
// Fabric (secondary); React DOM (primary) and React ART (secondary).
// Secondary renderers store their context values on separate fields.
_currentValue: defaultValue,
_currentValue2: defaultValue,
// Used to track how many concurrent renderers this context currently
// supports within in a single renderer. Such as parallel server rendering.
_threadCount: 0,
// These are circular
Provider: (null: any),
Consumer: (null: any),
// Add these to use same hidden class in VM as ServerContext
_defaultValue: (null: any),
_globalName: (null: any),
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
return context;
}
in the process of React compilation, the JSX syntax code we wrote will be converted into a React.createElement method, and after executing this method, we will get a ReactElement element object, which is what we call Virtual Dom. This element object records the input parameters and element types received by the current component.
and the Provide component is actually a ReactElement after compilation, but its Type is not the same as the normal component,
but context.Provider.
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
so how do subcomponents use Provider and useContext to get the latest data?
useContext takes a context object as a parameter and reads the value of the current contetx from the context._currentValue.
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
// get the value saved by the current context
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
// ...do something
// returns the current value
return value;
}
The
question arises again: when the state of the parent component changes, how does the Provider trigger the update and notify the subscribed child components for re-rendering?
when the status of the parent component is updated, the React as a whole enters the scheduling update phase, and the Fiber node enters the beginWork method, in which the corresponding method is executed according to the type of the current update node. As mentioned above, the Provider component has its own type ContextProvider, so it goes to the corresponding update method, updateContextProvide.
in fact, what you do in updateContextProvide can be summarized as follows:
first update the context._currentValue, then compare whether the new and old values have changed, and if not, jump out of the update function and reuse the current fiber node. If there is a change, call a method called propagateContextChange to deeply traverse the subcomponents of the Provider component, find the subcomponents that are subscribed to the current context, and mark them with the need to update, lane.
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const providerType: ReactProviderType<any> = workInProgress.type;
const context: ReactContext<any> = providerType._context;
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
pushProvider(workInProgress, context, newValue);
if (enableLazyContextPropagation) {
// In the lazy propagation implementation, we don't scan for matching
// consumers until something bails out, because until something bails out
// we're going to visit those nodes, anyway. The trade-off is that it shifts
// responsibility to the consumer to track whether something has changed.
} else {
if (oldProps !== null) {
const oldValue = oldProps.value;
if (is(oldValue, newValue)) {
// No change. Bailout early if children are the same.
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
}
} else {
// The context value changed. Search for matching consumers and schedule
// them to update.
propagateContextChange(workInProgress, context, renderLanes);
}
}
}
// do something...
}
so, in deep traversal, how do you know if the current subcomponent subscribes to the current Context?
in fact, when using useContext, in addition to reading the value of the current context, the received context object information will be saved in the Fiber.dependencies of the current component, so when traversing, you only need to see whether the current component has a subscription relationship on the dependencies of the current component.
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
} else {
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
if (lastContextDependency === null) {
lastContextDependency = contextItem;
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value;
}
can only subcomponents wrapped by Provider components read the state of Context?
in fact, all components can read the currentValue in the Context object through useContext, but only the components wrapped by the Provider component can subscribe to the value changes in the Context object and update the status of their components as they change. In fact, the purpose of this design is to better optimize the performance of React components in updating components. Just imagine, if every Context object is created, all components can subscribe to this Context change by default, then the entire Fiber tree needs to be traversed by Fiber nodes in the update process, and some components that are not needed and do not subscribe to the current Context also need to be traversed, which is actually a waste of performance.