React high level guidance: Context context & component combination & Render Props

Posted by stennchau on Tue, 01 Feb 2022 22:24:31 +0100

React high level guidance: Context context & component combination & render props

preface

Today is also to explain the advanced application skills of React. The content may involve the use of some special API s, or the special usage of React components and props. It can be regarded as a design pattern, an exclusive design pattern in React! (for those who are new to React, please move on.) Introduction to React: comprehensive analysis of core features)

The main topics to be explained in this article are

  • Use of Context
  • Component Composition
  • Render props function render component

At the same time, it will cooperate with some use scenarios and sample code. Let's start right away

text

1. Context

1.1 Motivation & scenario

Before we start, let's talk about why we need Context.

In the previous article: React advanced guidance: upgrade from state to high-level component (HOC) , we mentioned that when multiple leaf nodes need to share the state, they can promote the shared state to the nearest common parent component, as shown in the figure below

At that time, when our component nesting logic was very complex and the component rendering tree became higher and higher, it was more and more distant to find the nearest common parent component

At the same time, the state passed from the parent component needs to be passed down layer by layer according to the component nesting relationship. For the intermediate state independent components, there are not only a lot of props that are not related to themselves to be processed, but also a kind of destruction to the logic of the intermediate components.

At this time, we imagine whether there is a way to penetrate the intermediate components and directly transfer the state to the target components

This is the original motivation of the Context object.

Note: however, it is not advisable to abuse Context just to avoid simple props delivery, but let's take it as a goal for the moment. Later, we will explain why it is not advisable to simplify props delivery only

Let's take a look at the different ways and techniques of using Context one by one

1.2 basic usage: Provider + Consumer

First, let's introduce the basic usage of Provider + Consumer

1.2.1 define global data objects

First, we define a data to be shared. When Context is selected, it may be a global shared data object

  • src/context/themes.js
const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
}

export default themes

We define a global layout theme, which is divided into foreground and background colors of general mode and dark mode

1.2.2 React.createContext creates a context object ThemeContext

The next step is to use react The createcontext API creates our context object

  • src/context/ThemeContext.js
import React from 'react'
import themes from './themes'

export const ThemeContext = React.createContext(themes.dark)

React. The parameter of createcontext (defaultValue) passes in the default value. When our Provider component does not define data, it will use the default value passed in when creating the context object at the beginning

Let's see what the so-called Provider is

1.2.3 use themecontext Provider definition context

We have defined the context object and global data, so how do we add this global input to our component tree? The answer is through context Provider is a special component. Taking it as the root will create a local component tree with global data, that is, from context Providers will be able to directly obtain this global data object in some way

  • src/context/Version1.jsx
class Version1 extends Component {
  constructor(props) {
    super(props)
    this.state = {
      theme: themes.light,
    }
    this.toggleTheme = this.toggleTheme.bind(this)
  }

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

  render() {
    return (
      <div>
        <ThemeContext.Provider value={this.state.theme}>
          <ToolBar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
      </div>
    )
  }
}

We can see that the Version1 component first puts the theme data into the component state, and then uses themecontext when rendering Provider special component to create a local component tree with context, and pass in the global data through the value attribute.

As long as themecontext is next Components at any level under the provider can directly obtain the global data object passed in from value in some way

1.2.4 using themecontext Consumer gets global data

In the first example, we first show the most basic one: using context Consumer to obtain local global data objects. We use the ToolBar component as an intermediate component, indicating that the global data (theme) directly skips the ToolBar and is directly transferred from the Version1 component to the ThemedButton component for use

  • src/context/Version1.jsx
function ToolBar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change theme
    </ThemedButton>
  )
}
// Using Consumer
class ThemedButton extends Component {
  render() {
    const { children, onClick } = this.props
    return (
      <>
        {/* directly Usage Component */}
        <ThemeContext.Consumer>
          {(theme) => (
            <button
              onClick={onClick}
              style={{
                backgroundColor: theme.background,
                color: theme.foreground,
              }}
            >
              {children}
            </button>
          )}
        </ThemeContext.Consumer>
      </>
    )
  }
}

We can see that in the ThemedButton component, we use themecontext Consumer, a special component, can obtain themecontext The global object passed from the provider.

The specific way to receive data is to define a sub Component of Render Props (what is Render Props will be explained later), that is, define a function labeled value = > Component as a sub Component, and the value at this time is passed into themecontext at that time The global data object of the value attribute of the provider Component, and we can render the internal Component component according to the global data object value

The final effect is as follows

We can see that by clicking the button to call the toggleTheme just passed through the props stream, we can change the global theme data and then change the style of the button.

1.2.5 any component can be used as a consumer

The most basic context Although the usage of consumer is trivial, its function is quite comprehensive. Since the sub component rendered according to the global data value is a JSX, it can be either a class component or a function component

  • Consumer class components
// Using Consumer
class ThemedButton extends Component {
  render() {
    const { children, onClick } = this.props
    return (
      <>
        {/* Class Component */}
        <ThemeContext.Consumer>
          {(theme) => {
            const props = {
              children,
              onClick,
              style: {
                backgroundColor: theme.background,
                color: theme.foreground,
              },
            }
            return <StyledButton {...props} />
          }}
        </ThemeContext.Consumer>
      </>
    )
  }
}
class StyledButton extends Component {
  render() {
    console.log('styled button 1')
    const { children, ...props } = this.props
    return <button {...props}>{children}</button>
  }
}
  • Consumer is a function component
// Using Consumer
class ThemedButton extends Component {
  render() {
    const { children, onClick } = this.props
    return (
      <>
        {/* Function Component */}
        <ThemeContext.Consumer>
          {(theme) => {
            const props = {
              children,
              onClick,
              style: {
                backgroundColor: theme.background,
                color: theme.foreground,
              },
            }
            return <StyledButton2 {...props} />
          }}
        </ThemeContext.Consumer>
      </>
    )
  }
}
function StyledButton2(props) {
  console.log('styled button 2')
  const { children, ...rest } = props
  return <button {...rest}>{children}</button>
}

1.3 using contextType to simplify class components

However, use context The method of consumer is actually a little troublesome, and it is still a little huge to write, so static is also provided for class components How contexttype works

  • src/context/Version2.jsx
import React, { Component } from 'react'
import { ThemeContext } from './ThemeContext'
import themes from './themes'

class ThemedButton extends Component {
  // Use contextType
  static contextType = ThemeContext

  render() {
    const theme = this.context
    const { children, onClick } = this.props
    return (
      <button
        onClick={onClick}
        style={{
          backgroundColor: theme.background,
          color: theme.foreground,
        }}
      >
        {children}
      </button>
    )
  }
}

function ToolBar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change theme
    </ThemedButton>
  )
}

class Version2 extends Component {
  constructor(props) {
    super(props)
    this.state = {
      theme: themes.light,
    }
    this.toggleTheme = this.toggleTheme.bind(this)
  }

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

  render() {
    return (
      <div>
        <ThemeContext.Provider value={this.state.theme}>
          <ToolBar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
      </div>
    )
  }
}

export default Version2

The second version is the same as the first version, and its core lies in

class ThemedButton extends Component {
  // Use contextType
  static contextType = ThemeContext

When we define static contexttype for class components, we can directly use this Context gets the global data objects in the context as follows

class ThemedButton extends Component {
  // Use contextType
  static contextType = ThemeContext

  render() {
    const theme = this.context
    const { children, onClick } = this.props
    return (
      <button
        onClick={onClick}
        style={{
          backgroundColor: theme.background,
          color: theme.foreground,
        }}
      >
        {children}
      </button>
    )
  }

1.4 multiple Context

However, we can see that in the previous two examples, there is only one global data object. What can we do. In fact, we can simply nest context directly Provider:

1.4.1 new context object UserContext

First, we create a new global data

  • src/context/users.js
const users = {
  donovan: {
    name: 'Donovan',
    age: 22,
  },
  alice: {
    name: 'Alice',
    age: 18,
  },
}

export default users

Next, create a new context object

  • src/context/UserContext.js
import React from 'react'
import users from './users'

export const UserContext = React.createContext(users.donovan)

1.4.2 nested use of providers

Next, let's directly stack the two Provider components together

  • src/context/Version3.jsx
class Version3 extends Component {
  constructor(props) {
    super(props)
    this.state = {
      theme: themes.light,
      user: users.donovan,
    }
    this.toggleTheme = this.toggleTheme.bind(this)
    this.signIn = this.signIn.bind(this)
  }

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

  signIn(user) {
    this.setState({ user })
  }

  render() {
    return (
      <div>
        <ThemeContext.Provider value={this.state.theme}>
          <UserContext.Provider value={this.state.user}>
            <ToolBar
              toggleTheme={this.toggleTheme}
              signIn={this.signIn}
            />
          </UserContext.Provider>
        </ThemeContext.Provider>
      </div>
    )
  }
}

1.4.3 use different consumers to accept data

When there are multiple contexts, if we use the usage of contextType, we can only use one Context data type, because contextType can only have one type value.

If you want to use multiple global data objects at one time, you need to go back to Context According to the usage of Consumer, the Consumer provided by different Context objects will pass in the corresponding global data value, as follows:

  • src/context/Version3.jsx
// multiple context
function ToolBar(props) {
  const { toggleTheme, signIn } = props
  return (
    <>
      <ThemeContext.Consumer>
        {(theme) => (
          <button
            onClick={toggleTheme}
            style={{
              backgroundColor: theme.background,
              color: theme.foreground,
            }}
          >
            Change theme
          </button>
        )}
      </ThemeContext.Consumer>
      <br />
      <button onClick={() => signIn(users.donovan)}>
        Sign in as Donovan
      </button>
      <button onClick={() => signIn(users.alice)}>
        Sign in as Alice
      </button>
      <button onClick={() => signIn(null)}>Sign out</button>
      <UserContext.Consumer>
        {(user) => {
          return (
            <div>
              <h3 style={{ margin: '5px 0' }}>
                User: {user ? `${user.name}, ${user.age}` : ''}
              </h3>
            </div>
          )
        }}
      </UserContext.Consumer>
    </>
  )
}

We can see themecontext The Render Props of the consumer component passes in the theme global data; Usercontext The global data of user is passed in by the consumer. The final effect is as follows

1.5 transfer the state transition function through Context

Previously, we noticed that the methods of changing global data, such as toggleTheme and signIn, are passed down layer by layer through ordinary data stream props. In fact, we can also put the relevant global data update functions into the context object

  • src/context/Version4.jsx
class Version4 extends Component {
  constructor(props) {
    super(props)

    this.toggleTheme = this.toggleTheme.bind(this)

    this.state = {
      theme: themes.light,
      // State changes are also passed through the context
      toggleTheme: this.toggleTheme,
    }
  }

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

  render() {
    return (
      <div>
        <ThemeContext.Provider value={this.state}>
          <ToolBar />
        </ThemeContext.Provider>
      </div>
    )
  }
}

export default Version4

At this time, there will be no part related to global data in the intermediate component

function ToolBar() {
  return <ThemedButton>Change theme</ThemedButton>
}

Finally, get the global data and update function directly by setting contextType

class ThemedButton extends Component {
  static contextType = ThemeContext

  render() {
    const { theme, toggleTheme } = this.context
    return (
      <button
        onClick={toggleTheme}
        style={{
          backgroundColor: theme.background,
          color: theme.foreground,
        }}
      >
        {this.props.children}
      </button>
    )
  }
}

1.6 Context summary: why use less Context?

So far, we have seen various usage scenarios and usage methods of Context. Now let's go back to what we said earlier: don't use Context just to simplify props.

Context can indeed avoid the trouble of transmitting data through props, but there is a fatal defect in the implementation of context. In fact, all components related to Consumer / contextType are strongly associated with global data, so once the data changes, all components that depend on it will be forcibly updated.

In other words, if we cram the shared data that needs to be transmitted by props into the Context, it will become as follows:

<ThemeContext.Provider value={{
    props1: value1,
    props2: value2,
    props3: value2
}}>

It seems that there is no problem, but in fact, different components in the sub components may only depend on one of the data. However, the Context update data is global, that is, when we update props1, the sub components that depend on props2 and props3 may be re rendered together, resulting in additional and unnecessary rendering and waste of performance.

1.6.1 use specification (recommended)

So far, we have known the usage mode and defects of Context. In a word:

Context is used to transfer global data that is really needed by multiple components

When we just need to simplify the trouble of data transmission layer by layer through props in the component tree, we should use the method of component composition to be described in the next paragraph.

2. Component composition

As mentioned earlier, if we just want to avoid the risk of transferring props layer by layer, rather than using real global data, we should avoid using Context and use the concept of component composition.

2.1 what is component combination?

The core idea of component composition is that since we don't want the shared data to be transmitted layer by layer through props, we first transfer the data bound component into props at the top level, and the sub component only needs to specify the real location of the incoming component

That is, from the following form

function ComponentA() {
    const data = {/* ... */}
    return <ComponentB data={data} />
}

function ComponentB(props) {
    return <ComponentC data={props.data} />
}

function ComponentC(props) {
    return <div>{props.data.toString()}</div>
}

Become this form

function ComponentA() {
    const data = {/* ... */}
    return <ComponentB componentC={<ComponentC data={data} />} />
}

function ComponentB(props) {
    return props.componentC
}

function ComponentC(props) {
    return <div>{props.data.toString()}</div>
}

Even further

function ComponentA() {
    const data = {/* ... */}
    const componentC = <div>{props.data.toString()}</div>
    return <ComponentB componentC={componentC} />
}

function ComponentB(props) {
    return props.componentC
}

In this way, we do not need to transfer data through props, but directly transfer the data bound component to the specified location, which is the core concept of the component.

This approach is equivalent to an embodiment of inversion of control, which promotes the rendering logic of sub components to higher-level components, and in turn, the high-level components provide the implementation of sub components, while the original intermediate components only need to accept the components passed by the parent components and bind them to the correct position

Let's take a look at the different implementations of component composition

2.2 component combination formula 1: render sub node array

The first is the most common children attribute. In React, children is a very special attribute. When we use components and put data between component labels, it will actually appear as a member of the children attribute, that is to say, the following two implementations are equivalent

const component = <div>A Component</div>
const Wrapper = <div children={component} />

// Equivalent to

const component = <div>A Component</div>
const Wrapper = <div>{component}</div>

When children have multiple elements, they will naturally become an array, that is, the form of list rendering. The following is our demonstration code

  • src/composition/index.jsx
import React, { Component } from 'react'

import SideBar from './SideBar'
import './index.css'
import Header from './Header'
import Main from './Main'
import Footer from './Footer'

function MenuItem(props) {
  const { label, title, onClick } = props
  return (
    <div className="item" title={title} onClick={onClick}>
      {label}
    </div>
  )
}

class Composition extends Component {
  constructor(props) {
    super(props)
    this.state = {
      menuItems: [
        { label: 'Menu Item 1', title: 'go menu item 1' },
        { label: 'Menu Item 2', title: 'go menu item 2' },
        { label: 'Menu Item 3', title: 'go menu item 3' },
        { label: 'Menu Item 4', title: 'go menu item 4' },
        { label: 'Menu Item 5', title: 'go menu item 5' },
      ],
    }
  }

  handlerMenuItemSelect(item) {
    console.log('item', item)
  }

  render() {
    const items = this.state.menuItems.map((item) => (
      <MenuItem
        {...item}
        key={item.label}
        onClick={() => this.handlerMenuItemSelect(item)}
      />
    ))

    return (
      <div className="composition">
        <div className="container">
          <SideBar>{items}</SideBar>
          {/* ... */}
        </div>
      </div>
    )
  }
}

export default Composition

We can see that in the example, we directly assemble the navigation list needed by the sidebar according to the data in the top-level component

function MenuItem(props) {
  const { label, title, onClick } = props
  return (
    <div className="item" title={title} onClick={onClick}>
      {label}
    </div>
  )
}

// ...

    this.state = {
      menuItems: [
        { label: 'Menu Item 1', title: 'go menu item 1' },
        { label: 'Menu Item 2', title: 'go menu item 2' },
        { label: 'Menu Item 3', title: 'go menu item 3' },
        { label: 'Menu Item 4', title: 'go menu item 4' },
        { label: 'Menu Item 5', title: 'go menu item 5' },
      ],
    }

    // ...

    const items = this.state.menuItems.map((item) => (
      <MenuItem
        {...item}
        key={item.label}
        onClick={() => this.handlerMenuItemSelect(item)}
      />
    ))

Then put the list in the middle of the SideBar component, that is, pass it in as the children attribute

render() {
  return (
    <SideBar>{items}</SideBar>
    // ...

Finally, inside the SideBar component, it can be directly placed at the target position

  • src/composition/SideBar.jsx
import React, { Component } from 'react'

class SideBar extends Component {
  render() {
    return <div className="sidebar">{this.props.children}</div>
  }
}

export default SideBar

The effect is as follows

2.3 the second type of component combination: slot

Previously, we used the children attribute to pass sub components, but it is only a single attribute, which can not describe the location of sub components very accurately.

The second implementation method is to pass in the sub component through the props with the specified name, and then specify the corresponding position of the special sub component. This implementation is called slot:

  • src/composition/Header.jsx
import React, { Component } from 'react'

class Header extends Component {
  render() {
    const { left, right } = this.props
    return (
      <div className="header">
        <div className="left">{left}</div>
        <div className="center">
          <h2>Header</h2>
        </div>
        <div className="right">{right}</div>
      </div>
    )
  }
}

export default Header

First, we define a Header component and leave two slots, left and right, on both sides of the "Header" text

  • src/composition/index.jsx
function Title(props) {
  return <h3>{props.title}</h3>
}

function UserInfo(props) {
  return <h4>{props.username}</h4>
}

class Composition extends Component {
  render() {
    return (
      <div className="composition">
        <div className="container">
          <SideBar>{items}</SideBar>
          <div className="container vertical">
            <Header
              left={<Title title="This is a title for Header" />}
              right={<UserInfo username="Alice" />}
            />
          </div>
        </div>
      </div>
    )
  }
}

Next, we pass the Title component into the left attribute; Pass the UserInfo component into the right attribute, so that for external components, as long as I specify the passed in attribute, it is equivalent to passing in the specified location, without caring about the specific rendering location or additional props transmission. The effect is as follows

2.4 component combination formula 3: special examples

The third implementation is to extract some instances of special binding values in advance for different implementation effects.

When developing components, we usually follow a principle: try to make the bottom components as simple as possible. It's best to simply rely on props to realize result rendering, that is, we will define the following abstract components first:

  • src/composition/Footer.jsx
function ColoredBlock(props) {
  return (
    <div
      style={{
        width: '50px',
        height: '50px',
        backgroundColor: props.color,
      }}
    ></div>
  )
}

But each time you want to use it, you have to pass in the attribute or render the component according to a value passed down from a higher-level component. In fact, we can also define several component instances that pass in specific values in advance, and make these instances into another component as follows

function SkyBlueBlock() {
  return <ColoredBlock color="skyblue" />
}

function CoralBlock() {
  return <ColoredBlock color="coral" />
}

function LimeBlock() {
  return <ColoredBlock color="limegreen" />
}

function CrimsonBlock() {
  return <ColoredBlock color="crimson" />
}

In this way, when we use it, we no longer need to pass in the color attribute for binding, but just use it directly as if we were using a static component

class Footer extends Component {
  render() {
    return (
      <div className="footer">
        <LimeBlock />
        <SkyBlueBlock />
        footer
        <CoralBlock />
        <CrimsonBlock />
      </div>
    )
  }
}

The effect is as follows

2.5 component composition formula 4: Render Props transfer rendering function

The last implementation is special. Remember that when we use props to pass in the components with bound attributes, we pass in a component instance directly, and then directly place the components in a fixed position in the sub components. At this time, can we also pass in a function to delay the components to the sub components for binding? Now let's try

  1. First, we define a component that tracks the position of the mouse
  • src/composition/Main.jsx
class Mouse extends Component {
  constructor(props) {
    super(props)
    this.state = {
      x: 0,
      y: 0,
    }
    this.handleMouseMove = this.handleMouseMove.bind(this)
  }

  handleMouseMove(e) {
    this.setState({
      x: e.clientX,
      y: e.clientY,
    })
  }

  render() {
    const { x, y } = this.state
    return (
      <div className="backbone" onMouseMove={this.handleMouseMove}>
        <span
          style={{ position: 'relative', top: '10px', left: '10px' }}
        >
          mouse position: ({x}, {y})
        </span>
      </div>
    )
  }
}
  1. Then use it directly in the parent component
class Main extends Component {
  render() {
    return (
      <div className="main">
        <Mouse />
      </div>
    )
  }
}

The effect is as follows

Next, we want to render a box and move it with the mouse.

The first is the block class

function Square(props) {
  const { x, y } = props.position
  const width = 100
  return (
    <div
      style={{
        backgroundColor: 'skyblue',
        position: 'fixed',
        left: x - width / 2,
        top: y - width / 2,
        width: width,
        height: width,
      }}
    />
  )
}

Next, we may want to put it directly into the Mouse component as follows

class Mouse extends Component {
  // ...

  render() {
    const { x, y } = this.state
    return (
      <div className="backbone" onMouseMove={this.handleMouseMove}>
        <span
          style={{ position: 'relative', top: '10px', left: '10px' }}
        >
          mouse position: ({x}, {y})
        </span>
        <Square position={this.state} />
      </div>
    )
  }
}

However, there is a problem that the Mouse component is strongly coupled with the Square component, which we don't want to see. When we need to change the Square component in the Mouse component into other components, we will encounter big trouble.

At this time, we can use the concept of Render Props to import the required component (Square) through the parent component; Further, this component can still be associated with a sub component (Mouse). At this time, we cannot directly pass in the component instance, but a Render Props, that is, a method of delaying the binding of the component instance, as follows

class Main extends Component {
  render() {
    return (
      <div className="main">
        <Mouse
          render={(position) => <Square position={position} />}
        />
      </div>
    )
  }
}

It's right that we want to render Square in Mouse, but we can't render it until we get the location information in Mouse, so we pass in a function that accepts position as the parameter to generate a real Square instance, and we can write this inside Mouse

class Mouse extends Component {
  // ...

  render() {
    const { x, y } = this.state
    return (
      <div className="backbone" onMouseMove={this.handleMouseMove}>
        <span
          style={{ position: 'relative', top: '10px', left: '10px' }}
        >
          mouse position: ({x}, {y})
        </span>
        {this.props.render(this.state)}
      </div>
    )
  }
}

As we can see, although Mouse doesn't know who is the component finally rendered to this location, I know that as long as I call props The render method and passing in its own location information can return a component bound with its own Mouse information, which is Render Props

The effect is as follows

Note: the meaning of Render Props here is just "pass in a function that can generate a real component instance", that is, we don't have to use the render attribute. Any attribute is good. The core idea is to pass in a design pattern that generates a real component according to the sub component information.

Is it very similar to the previous context What about the usage of consumer?

render() {
    return (
        <Context.Consumer>
            {value => <Component />}
        </Context.Consumer>
    )
}

Yes, actually, context Consumer uses the method of Render Props to transfer global data into the user-defined value = > component function to bind the global data completely.

epilogue

This chapter introduces the usage of Context and some examples of component combination. Finally, it introduces the characteristics and usage of Render Props and Specific implementation in consumer. These are very practical features in actual development for your reference.

Other resources

Reference connection

TitleLink
React official - Contexthttps://react.docschina.org/docs/context.html
React official - combination vs inheritancehttps://react.docschina.org/docs/composition-vs-inheritance.html
React official - Render Propshttps://react.docschina.org/docs/render-props.html
Use of Context and contextType in Reacthttps://blog.csdn.net/landl_ww/article/details/93514944

Complete code example

https://github.com/superfreeeee/Blog-code/tree/main/front_end/react/react_context_component_composition_render_props

Topics: Javascript React Context