preface

In a typical React application, data is passed from top to bottom through the props property, which is also known as parent to child. But in some cases (skin), many of the underlying child components need to receive skin properties from the top component, which gives us a lot of places to show props in our code. Context provides a way to share such values between components without explicitly passing props layer by layer through the component tree.

When do you use Context

Context is used when components at different levels need to access the same data.

Here’s an example:

The scenario looks like this: the root component of a Page is a Page, and we need to layer down user and avatarSize to the target component so that the deeply nested Avatar and Info components can read these properties.

<Page user={user} avatarSize={avatarSize} />
 / /... Apply colours to a drawing gives...

<PageLayout user={user} avatarSize={avatarSize} />
 / /... Apply colours to a drawing gives...

<NavigationBar user={user} avatarSize={avatarSize} />
 / /... Apply colours to a drawing gives...

<Avatar user={user} size={avatarSize} />
<Info user={user} size={avatarSize} />
Copy the code

If only the Avatar and Info components end up using these two properties, this layer-by-layer approach can appear redundant. If we need to add attributes like color and background, we need to add them one by one in the middle layer.

The React document provides a context-free solution that uses component composition to pass the Avatar and Info to the Page component.

 function Page(props) {
   const user = props.user;
   const userComponent = (
     <div>
       <Avatar user={user} size={props.avatarSize} />
       <Info user={user} size={props.avatarSize} />
     </div>
   );
   return <PageLayout userComponent={userLink} />;
 }

 <Page user={user} avatarSize={avatarSize} />
 / /... Apply colours to a drawing gives...
 <PageLayout userComponent={... } />
 / /... Apply colours to a drawing gives...
 <NavigationBar userComponent={... } />
 / /... Apply colours to a drawing gives...
 {props.userComponent}
Copy the code

In this way, we can reduce the passing of useless props in our React code. But there is a downside to this: the top-level components can become very complex.

So in this case, is Context a best practice?

The answer is not necessarily. Because once you use the Context, the reuse rate of components becomes very low.

The Context API before 16.x has a bad point: if the PageLayout props changes but shouldComponentUpdate returns false during its lifecycle, the Avatar and Info values cannot be updated. However, this problem does not occur with new versions of the Context API after 16.x. We will talk about how this works later, but we will briefly mention it here.

If our component doesn’t need to be reused, then I think using the Context is the best practice for now. I think the translation of the Context on the website says this very well: Context allows you to broadcast that data to all the components in the component tree, all the components have access to that data, I can access subsequent data updates. When I first learned this sentence, I had a clear and deep understanding of Context, but as a front-end engineer, we never stop there.

import React, { Component, createContext, PureComponent } from 'react';

const SizeContext = createContext();

class User extends Component {
 render() {
   return <SizeContext.Consumer>
     {
       value => <span style={{ fontSize: value}} >I am the maple</span>
     }
   </SizeContext.Consumer>}}class PageLayout extends PureComponent {
 render() {
   return <NavigationBar />}}class NavigationBar extends PureComponent {
 render() {
   return <User/>}}class Page extends Component {
 state = {
   fontSize: 20
 }
 render() {
   const { fontSize } = this.state;
   return (
     <SizeContext.Provider value={fontSize}>
       <button
         type="button"
         onClick={()= >This.setstate ({fontSize: fontSize + 1})} > increases the fontSize</button>
       <button
         type="button"
         onClick={()= >This.setstate ({fontSize: fontsize-1})} > reduce fontSize</button>
       <PageLayout />
     </SizeContext.Provider>); }}export default Page;
Copy the code

By using the Context, we can make our code more elegant. And if you think that’s not a very elegant way to write it

  <SizeContext.Consumer>
    {
      value= > <span style={{ fontSize: value}} >I am the maple</span>
    }
  </SizeContext.Consumer> 
Copy the code

Then we can use contextType to further optimize the code

class User extends Component {
  static contextType = SizeContext;
  render() {
    return <span style={{ fontSize: this.context}} >I am the maple</span>}}Copy the code

Note: It only supports a single Context, and if multiple contexts are used, you can only use nested methods.

If we want to define multiple contexts, let’s say a new color Context. We simply add a Provider to the Page component and a Consumer to the Consumer.

  <SizeContext.Provider value={fontSize}>
    <button
      type="button"
      onClick={()= >This.setstate ({fontSize: fontSize + 1})} > increases the fontSize</button>
    <button
      type="button"
      onClick={()= >This.setstate ({fontSize: fontsize-1})} > reduce fontSize</button>
    <ColorContext.Provider value="red">
    <PageLayout />
    </ColorContext.Provider>
  </SizeContext.Provider>
Copy the code
  <SizeContext.Consumer>
    {
      fontSize= > <ColorContext.Consumer>{color => <span style={{ fontSize.color}} >I am the maple</span> }</ColorContext.Consumer>
    }
  </SizeContext.Consumer>
Copy the code

The programming experience for consumers of multiple contexts is actually not very friendly. After Hook appeared, useContext API was also introduced to help us solve this problem.

const User = () = > {
  const fontSize = useContext(SizeContext);
  const color = useContext(ColorContext);
  return (
    <span style={{ fontSize.color}} >I am the maple</span>
  );
};
Copy the code

Here, I think there are two points worth thinking about:

  • How does a new Context after 16.X address issues like shouldComponentUpdate

  • How does a Function Component subscribe to multiple contexts

Next, we will take you through the mystery and explore how the new Context is implemented.

Implementation principle of the new Context

Reactfiberbeginwork.js reactFiberBeginwork.js reactFiberBeginwork.js It doesn’t matter if you haven’t read the source code before, for this article you need to know that this method is used to update every node of the tree. So our source code parsing begins with beginWork.

The Context of the design

 var context = {
    ?typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    Provider: null.Consumer: null
  };
Copy the code

CreateContext creates a context object that contains a Provider and a Consumer.

The Provider’s _context points to the context itself.

context.Provider = {
  $$typeof: REACT_PROVIDER_TYPE,
  _context: context,
};
Copy the code

Now let’s look at Consumer, whose _context also points to itself

 const Consumer = {
    $$typeof: REACT_CONTEXT_TYPE,
    _context: context,
    _calculateChangedBits: context._calculateChangedBits,
  };
Copy the code

The Provider of the update

When we render for the first time, the current node in the Fiber tree will definitely not exist, so we will not go if (current! == null) { // … } The logic behind this. When we create colorContext. Provider, React will tag our Fiber node with a ContextProvider WorkTag. The other nodes are similar, with a total of 18 tags.

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
Copy the code

  • Update Provider for the first time

When React processes the Provider node, the updateContextProvider method is called to update the Provider

Because this is the first rendering, currently the props on Fiber is empty and there is no memoizedProps. Therefore, we have implemented only pushProvider and reconcileChildren in this step. First call pushProvider to push the current Fiber and its props onto the Stack. During the React update, there is a fiberStack module that stores context as the tree is traversed.

After the push is complete, the current value of the context is set to the value passed in.

if (isPrimaryRenderer) {
  push(valueCursor, context._currentValue, providerFiber);
  context._currentValue = nextValue;
}
Copy the code

At this point, the value on the Consumer has actually been updated.

Of course if pushProvider is not the first update it will change _currentValue2 to the latest value.

  push(valueCursor, context._currentValue2, providerFiber);
  context._currentValue2 = nextValue;
Copy the code

Finally, execute the reconcileChildren to assign the result to workinProgress.child.

  • The Provider is not updated for the first time

When the Provider is updated again, the program enters oldProps! = = null

if(oldProps ! = =null) {
  const oldValue = oldProps.value;
  // Calculate the changes of props on the old and new context
  const changedBits = calculateChangedBits(context, newValue, oldValue);
  if (changedBits === 0) {
    // No change. Bailout early if children are the same.
    if( oldProps.children === newProps.children && ! hasLegacyContextChanged() ) {returnbailoutOnAlreadyFinishedWork( current, workInProgress, renderExpirationTime, ); }}else {
    // The context value changed. Search for matching consumers and schedule
    // them to update.propagateContextChange( workInProgress, context, changedBits, renderExpirationTime, ); }}Copy the code

Whether the program is updated depends on the calculated value of calculateChangedBits,

export function calculateChangedBits<T> (context: ReactContext
       
        , newValue: T, oldValue: T,
       ) {
 if( (oldValue === newValue && (oldValue ! = =0 || 1 / oldValue === 1 / (newValue: any))) || // exclude + 0 and -0(oldValue ! == oldValue && newValue ! == newValue)// eslint-disable-line no-self-compare excludes NaN
 ) {
   // No change
   return 0;
 } else {
   const changedBits =
     typeof context._calculateChangedBits === 'function'
       ? context._calculateChangedBits(oldValue, newValue)
       : MAX_SIGNED_31_BIT_INT;

   return changedBits | 0; }}Copy the code

We can see that in this code, if the props are unchanged and +0 and -0 and NaN are excluded, the result returns 0. Everything else is subject to update. A node update calls the propagateContextChange function

After rendering first, we have assigned the subtree to workinProgress.Child. So for the second update, let fiber = workinprogress.child; Take the subtree.

At this point, we find all nodes with firstContextDependency by traversing all nodes on Fiber.

The initial assignment of firstContextDependency is in the readContext method, which we will refer to later when we talk about Consumer.

React creates an update createUpdate(), labels it with ForceUpdate to force the update, and then pushes it to the queue. To ensure that it will be executed within this update rendering cycle, we change the value of expirationTime on Fiber to the expirationTime that is currently being updated.

Finally update the subtree on workInProgress.

The renewal of the Consumer

The Consumer update is more pure and involves three main phases: prepareToReadContext(ready to read the Context) readContext(start reading the readContext) 3. Rendering of child nodes

  • prepareToReadContext

    React empties the tree of dependencies every time the Context is read

    export function prepareToReadContext(workInProgress: Fiber, renderExpirationTime: ExpirationTime,) :void {
      currentlyRenderingFiber = workInProgress;
      lastContextDependency = null;
      lastContextWithAllBitsObserved = null;
    
      // Reset the work-in-progress list
      workInProgress.firstContextDependency = null;
    }
    Copy the code
  • readContext

    In readContext, all dependent contexts on Fiber are stored in a linked list. After the next update, the Provider side will be able to retrieve the context it depends on via the Fiber node.

Hook useContext parsing

All hooks under a Hook are dispatched via dispatch

const HooksDispatcherOnRerender: Dispatcher = {
  readContext,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: rerenderReducer,
  useRef: updateRef,
  useState: rerenderState,
  useDebugValue: updateDebugValue,
  useDeferredValue: rerenderDeferredValue,
  useTransition: rerenderTransition,
  useMutableSource: updateMutableSource,
  useOpaqueIdentifier: rerenderOpaqueIdentifier,

  unstable_isNewReconciler: enableNewReconciler,
};
Copy the code

In the above code, you are actually calling the readContext API while using the Hook. It calls the underlying API directly, allowing us to get the value of the latest context directly.

When we did pushProvider, we assigned the value to context _currentValue and _currentValue2, respectively. So when we call useContext readContext is already returning the latest value of the current context.

function readContext() {
  // ...
return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
Copy the code