React Introduction Series (3) Context

In this blog we continue to look at the Context section of the React Advanced Guide.

This will be interspersed with component composition, render prop, and some knowledge of function components.

Why use context?

Context provides a way to pass data between component trees without manually adding props for each layer of components.

In a typical React application, data is passed from top to bottom (by parent and child) through props properties, but this usage is extremely cumbersome for certain types of properties (such as locale preferences, UI themes), these properties are required by many components in the application. Context provides a way to share such values between components without explicitly passing props layer by layer through the component tree.

From my understanding, if a property needs to be shared by multiple components and passed layer by layer, we can use this method.

Why meet these two conditions?

  • If it is simply common to multiple sibling components, we can use the state promotion method.

  • If we simply pass it layer by layer, we can also use a combination of components.

When to use

Context is designed to share data that is “global” to a component tree, such as the currently authenticated user, theme, or preferred language. For example, in the following code, we manually adjust the style of a button component through a “theme” attribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}

function Toolbar(props) {
//The Toolbar component takes an additional "theme" property and passes it to the ThemedButton component.
//If each individual button in the application needs to know the value of theme, this will be a very troublesome thing.
Because this value must be passed layer by layer to all components.
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}

class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}

Using context, we can avoid passing props through intermediate elements:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//Context allows us to pass values deep into the component tree without explicitly passing them through each component.
//Create a context for the current theme ("light" is the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
//Use a Provider to pass the current theme to the following component tree.//No matter how deep, any component can read this value.//In this example, we pass "dark" as the current value.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}

The middle component no longer has to specify the theme to be passed down.
function Toolbar() { return (
<div>
<ThemedButton />
</div>
);
}

class ThemedButton extends React.Component {
Specify contextType to read the current theme context.
//React will go up to the nearest theme provider and use its value.
//In this example, the current theme value is "dark".
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}

Note that in order to use this.context, you must first define static.

For example, ‘static’ in the above example

Use

The main use case for Context is that * many * components at different levels need to access some of the same data. Use caution as this makes components less likely to reuse.

** If you just want to avoid passing some properties layer by layer,组件组合(component composition)Sometimes a better solution than context. **

Component combination

For example, consider a Page component that passes down the user and avatarSize properties so that deeply nested Link and Avatar components can read them:

1
2
3
4
5
6
7
8
9
<Page user={user} avatarSize={avatarSize} />
//... render...
<PageLayout user={user} avatarSize={avatarSize} />
//... render...
<NavigationBar user={user} avatarSize={avatarSize} />
//... render...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>

If in the end only the Avatar component really needs user and avatarSize, then passing these two props layer by layer is very redundant. And once the Avatar component needs more props from the top-level component, you have to add them one by one at the middle level, which will become very troublesome.

A ** no context ** solution isAvatar 组件自身传递下去Therefore, the intermediate component does not need to know props such as’user ‘or’avatarSize’.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}

//Now, we have this component:
<Page user={user} avatarSize={avatarSize} />
//... render...
<PageLayout userLink={...} />
//... render...
<NavigationBar userLink={...} />
//... render...
{props.userLink}

With this change, only the topmost Page component needs to know how the Link and Avatar components use user and avatarSize.

This inversion of control over components reduces the number of props to pass in your application, which in many cases will make your code cleaner and give you more control over the root component. However, this doesn’t apply to every scenario: this kind of lifting the logic to a higher level in the component tree will make these high-level components more complex and force low-level components to adapt to such a form, which may not be what you want.

And your component is not limited to receiving a single subcomponent. You may pass multiple subcomponents and even encapsulate multiple separate “slots” for these subcomponents (children),正如这里的文档所列举的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Page(props) {
const user = props.user;
const content = <Feed user={user} />;
const topBar = (
<NavigationBar>
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
</NavigationBar>
);
return (
<PageLayout
topBar={topBar}
content={content}
/>
);
}

This mode is enough to cover many scenarios where you need to decouple the child component from the directly related parent component. If the child component needs to communicate with the parent component before rendering, you can further use render props

However, sometimes components at different levels in the component tree need to access the same batch of data. Context allows you to “broadcast” this data to all components in the component tree, and all components can access this data and subsequent data updates. Common scenarios for using context include managing the current locale, theme, or some cached data, which is much simpler than the alternative.

I don’t know if you will be a little confused when you see this doc, let me briefly sort it out:

render

A component with a render prop accepts a function that returns a React element and implements its rendering logic inside the component by calling this function.

1
2
3
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>

This is just a brief mention of the concept, and I will analyze the specific use of render props in a future blog.

API

React.createContext

1
const MyContext = React.createContext(defaultValue);

Create a Context object. When React renders a component that is subscribed to this Context object, the component will read the current context value from the nearest matching’Provider 'in the component tree.

The defaultValue parameter of the component will only take effect when there is no match to the Provider in the tree where it is located. This default value helps to test the component without wrapping it with a Provider. Note: When passing’undefined ‘to the value of the Provider, the’defaultValue’ of the consuming component will not take effect.

Context.Provider

1
< MyContext. Provider value = {/* some value */} >

Each Context object returns a Provider React component that allows the consuming component to subscribe to changes in the context.

Provider receives a’value 'attribute and passes it to the consumer component. A Provider can have a corresponding relationship with multiple consumer components. Multiple Providers can also be used nested, and the inner layer will overwrite the outer layer of data.

When the provider’s value changes, all consumer components inside it will be re-rendered. Neither the provider nor its internal consumer components are subject to the shouldComponentUpdate function, so consumer components can be updated if their ancestor components exit the update.

To determine the change by detecting the old and new values, the Object.is The same algorithm.

Attention

When passing an object to

Class.contextType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* After the component is mounted, use the value of the MyContext component to perform some side effects */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* Render based on the value of the MyContext component */
}
}
MyClass.contextType = MyContext;

The’contextType 'property mounted on the class will be reassigned to a React.createContext() Context object created. This property allows you to use this.context to consume the value of the most recent Context. You can access it in any lifetime, including the render function.

Note:

You only pass the

If you are using experimental

1
2
3
4
5
6
7
class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* Render based on this value */
}
}

Context.Consumer

1
2
3
<MyContext.Consumer>
{Value => /* render based on the context value */}
</MyContext.Consumer>

A React component that allows you to subscribe to changes in context函数式组件You can subscribe to context in.

This method requires a函数作为子元素(function as a child)This function takes the current context value and returns a React NodeThe value passed to the function is equivalent to the value provided by the provider closest to the context in the component treeIf there is no corresponding Provider, the value parameter is equivalent to the defaultValue passed to createContext ()

Attention

Want to learn more about

Context.displayName

The context object accepts a property called’displayName 'of type string. React DevTools uses this string to determine what the context should display.

For example, the following component will be displayed as MyDisplayName in DevTools:

1
2
3
4
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

Here we summarize the use and precautions regarding these APIs.

  • There is one way to create a Context, which is to call’const MyContext = React.createContext (defaultValue); ’
  • There are two ways to subscribe to Context:
    • In the component you want to subscribe to ‘static contextType = MyContext;’ but this method can only subscribe to one Context
    • ‘Context. Consumer’: This method requires a function as a child. This function receives the current context value and returns a React node.
  • The context object accepts a property called’displayName 'of type string. React DevTools uses this string to determine what the context should display.

Example

Dynamic

A more complex solution is to use dynamic values for the theme example above:

theme-context.js

1
2
3
4
5
6
7
8
9
10
11
12
export const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};

export const ThemeContext = React.createContext( themes.dark // 默认值);

A Context is declared here, the default value is theme.dark

themed-button.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {ThemeContext} from './theme-context';

class ThemedButton extends React.Component {
render() {
let props = this.props;
let theme = this.context;
return (
<button
{...props}
style={{backgroundColor: theme.background}}
/>
);
}
}
ThemedButton.contextType = ThemeContext;
export default ThemedButton;

This declares a component called’ThemedButton’

Via ThemedButton.contextType

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';

//An intermediate component using ThemedButton
function Toolbar(props) {
return (
<ThemedButton onClick={props.changeTheme}>
Change Theme
</ThemedButton>
);
}

class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light,
};

this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme = themes.dark
? themes.light
: themes.dark,
}));
};
}

render() {
//The ThemedButton component inside the ThemeProvider uses the theme value in state,
//while external components use the default theme value
return (
<Page>
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<Section>
<ThemedButton />
</Section>
</Page>
);
}
}

ReactDOM.render(<App />, document.root);

First of all, ‘ThemeContext. Provider’ provides value, so its internal Toolbar uses value instead of the default value of ThemeContext

The’ThemedButton ‘directly used below is not inside any’ThemeContext. Provider’ and directly uses the latest default value.

Update in nested components

It is necessary to update the context from a component that is deeply nested in the component tree. In this scenario, you can pass a function through context to make the consumers component update the context:

theme-context.js

1
2
3
4
5
//Make sure the default value data structure passed to createContext matches that of the invoked consumers!
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {}
});

theme-toggler-button.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
//The Theme Toggler button not only gets the theme value,//it also gets a toggleTheme function from the context
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}

export default ThemeTogglerButton;

According to our previous analysis, the children of’ThemeContext. Consumer 'are a function component. When the Context changes, it will regenerate the component.

That is to say, when our theme or toggleTheme changes, it will use the changed value to create a new component to replace the old component.

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
constructor(props) {
super(props);

this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme = themes.dark
? themes.light
: themes.dark,
}));
};

//State also contains an update function, so it is passed to the context provider.
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme,
};
}

render() {
The entire state is passed to the provider
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}

function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}

ReactDOM.render(<App />, document.root);

Content is rendered in’ThemeContext. Provider ‘, and’ThemeTogglerButton’ is rendered in Content.

The value provided by ThemeContext. Provider is its own state. This state contains the theme and toggleTheme functions.

When we trigger toggleTheme in’ThemeTogglerButton ', we actually modify the outermost state.

This state will be passed as a value by’ThemeContext. Provider ‘, triggering the update of’ThemeTogglerButton’.

This method is essentially no different from state promotion. It allows child components to have a way to change the state of the parent component, but this state does not need to be passed down layer by layer.

Consume multiple

To ensure fast re-rendering of context, React needs to make the context of each consumer component a separate node in the component tree.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//Theme context, default theme is "light" value
const ThemeContext = React.createContext('light');

//user login context
const UserContext = React.createContext({
name: 'Guest',
});

class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;

//App component that provides the initial context value
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}

function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}

A component may consume multiple contexts.
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}

If two or more context values are often used together, you may want to consider creating your own rendering component to provide these values.

Precautions

Because the context uses the reference identity to decide when to render, there may be some pitfalls here. When the provider’s parent component renders, it may trigger unexpected rendering in the consumers component. For example, every time the Provider re-renders, the following code will re-render all the following consumers components because the’value 'attribute is always assigned a new object:

1
2
3
4
5
6
7
8
9
class App extends React.Component {
render() {
return (
<MyContext.Provider value={{something: 'something'}}>
<Toolbar />
</MyContext.Provider>
);
}
}

To prevent this, promote the value state to the state of the parent node.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}

render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}

In this way, each time the Provider re-renders, it will not reassign value to a new object, but will always be state.value.