天天看点

React 高阶指引: Context 上下文 & 组件组合 & Render PropsReact 高阶指引: Context 上下文 & 组件组合 & Render Props前言正文结语其他资源

React 高阶指引: Context 上下文 & 组件组合 & Render Props

文章目录

  • React 高阶指引: Context 上下文 & 组件组合 & Render Props
  • 前言
  • 正文
    • 1. Context 上下文
      • 1.1 使用动机 & 场景
      • 1.2 基本用法:Provider + Consumer
        • 1.2.1 定义全局数据对象
        • 1.2.2 React.createContext 创建上下文对象 ThemeContext
        • 1.2.3 使用 ThemeContext.Provider 定义上下文
        • 1.2.4 使用 ThemeContext.Consumer 获取全局数据
        • 1.2.5 任意组件都能作为消费者
      • 1.3 使用 contextType 简化类组件
      • 1.4 多个 Context 上下文
        • 1.4.1 新的上下文对象 UserContext
        • 1.4.2 嵌套使用 Provider
        • 1.4.3 使用不同的 Consumer 接受数据
      • 1.5 将状态转换函数也透过 Context 传递
      • 1.6 Context 小结:为什么少用 Context?
        • 1.6.1 使用规范(推荐)
    • 2. 组件组合(Component Composition)
      • 2.1 什么叫组件组合?
      • 2.2 组件组合第一式:渲染子节点数组
      • 2.3 组件组合第二式:插槽(slot)
      • 2.4 组件组合第三式:特殊实例
      • 2.5 组件组合第四式:Render Props 传递渲染函数
  • 结语
  • 其他资源
    • 参考连接
    • 完整代码示例

前言

今天一样也是来解说 React 的高级应用技巧,内容可能涉及一些特别的 API 使用,又或是针对 React 组件和 props 的特殊用法,可以算作一种设计模式,React 内专属的设计模式!(初入 React 领域的同学请移步React 入门: 核心特性全面解析)

本篇要说明的主题主要有

  • Context 上下文的使用
  • Component Composition 组件组合
  • Render props 函数渲染组件

同时会配合一些使用场景和示例代码,下面我们马上开始

正文

1. Context 上下文

1.1 使用动机 & 场景

在开始用之前我们先来谈谈为什么要有 Context 上下文这种东西。

在上一篇:React 高级指引: 从状态提升到高阶组件(HOC),我们提过当多个叶节点需要共享状态的时候可以透过将共享的状态提升到最近的共同父组件当中如下图

React 高阶指引: Context 上下文 & 组件组合 & Render PropsReact 高阶指引: Context 上下文 & 组件组合 & Render Props前言正文结语其他资源

当时当我们的组件嵌套逻辑非常复杂,组件渲染树变得越来越高的时候,要找到最近的共同父组件越来愈遥远

React 高阶指引: Context 上下文 & 组件组合 & Render PropsReact 高阶指引: Context 上下文 & 组件组合 & Render Props前言正文结语其他资源

同时从父组件传递下来的状态需要根据组件嵌套关系一层层传递下来,对于中间的状态无关组件来说,不仅仅是多了好多与自己不相关的 props 需要处理,同时这也是对中间组件的逻辑的一种破坏。

这时候我们就设想能不能有一种方法能够穿透中间组件,直接将状态传递到目标组件当中

React 高阶指引: Context 上下文 & 组件组合 & Render PropsReact 高阶指引: Context 上下文 & 组件组合 & Render Props前言正文结语其他资源

而这就是 Context 上下文对象的原始动机。

备注:然而仅仅为了避免简单的 props 传递而滥用 Context 是不可取的,不过我们暂且先当作目标就是这么个回事,后面会在重新说明为什么仅仅用于简化 props 传递是不可取的

下面我们就一个个来看 Context 的不同使用方式和技巧

1.2 基本用法:Provider + Consumer

首先第一种我们先介绍最基础的 Provider + Consumer 的基本使用方式

1.2.1 定义全局数据对象

首先我们先定义一个需要被共享的数据,而在选择使用 Context 的情况下,其实它可能是某个全局的共享数据对象

  • src/context/themes.js

const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
}

export default themes
           

我们定义一个全局布局主题,分为一般模式和暗黑模式的前景和后景颜色

1.2.2 React.createContext 创建上下文对象 ThemeContext

接下来是使用

React.createContext

API 创建我们的上下文对象

  • src/context/ThemeContext.js

import React from 'react'
import themes from './themes'

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

React.createContext(defaultValue)

的参数传入的是默认值,当我们的

Provider

组件没有定义数据时则会使用一开始创建上下文对象时传入的默认值

defaultValue

下面我们来看看所谓的

Provider

是什么

1.2.3 使用 ThemeContext.Provider 定义上下文

前面我们已经定义好了上下文对象和全局数据,那么我们要如何将这个全局输入加入我们的组件树当中呢?答案就是透过

Context.Provider

这个特殊组件,以它为根会创建一个存在全局数据的局部组件树,也就是从

Context.Provider

都将能够透过某种方式直接获取这个全局数据对象

  • 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>
    )
  }
}
           

我们看到 Version1 组件首先先将主题(theme)数据放到组件状态当中,然后渲染的时候透过

ThemeContext.Provider

特殊组件来创建含有上下文的局部组件树,并将全局数据透过

value

属性传入。

接下来只要是在

ThemeContext.Provider

之下任意层级的组件都能透过某种方式直接获取从

value

传入的全局数据对象

1.2.4 使用 ThemeContext.Consumer 获取全局数据

在第一个例子中我们先展示最基础款的:使用

Context.Consumer

来获取局部的全局数据对象,我们使用

ToolBar

组件充当中间组件,说明全局数据(

theme

)直接跳过

ToolBar

Version1

组件直接传到

ThemedButton

组件中使用

  • src/context/Version1.jsx

function ToolBar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change theme
    </ThemedButton>
  )
}
           
// 使用 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>
      </>
    )
  }
}
           

我们可以看到,在

ThemedButton

组件内我们透过使用

ThemeContext.Consumer

这个特殊组件就能获得由

ThemeContext.Provider

传递下来的全局对象。

具体接受数据的方式是透过定义一个 Render Props 的子组件(后面会再解释什么是 Render Props),也就是定义一个标签为

value => Component

的函数作为子组件,这时候的

value

就是的当时传入

ThemeContext.Provider

组件的

value

属性的全局数据对象,而我们就可以根据这个全局数据对象

value

来渲染内部组件 Component

最终的效果如下

React 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render PropsReact 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render Props前言正文结语其他资源

我们可以看到透过点击按钮调用刚刚透过 props 流传递下来的

toggleTheme

就能够改变全局的

theme

数据,进而造成按钮的样式改变。

1.2.5 任意组件都能作为消费者

最基础的

Context.Consumer

的用法虽然琐碎,确实功能上比较全面的,根据全局数据

value

渲染的子组件由于就是一个 JSX,所以不论是类组件或是函数组件都可以

  • 消费者为 类组件
// 使用 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
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 使用 contextType 简化类组件

然而使用

Context.Consumer

的方式其实还是有点麻烦,而且写起来还是有些庞大,所以对于类组件还提供了

static.contextType

的方式

  • src/context/Version2.jsx

import React, { Component } from 'react'
import { ThemeContext } from './ThemeContext'
import themes from './themes'

class ThemedButton extends Component {
  // 使用 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
           

第二个版本与第一个版本雷同,其核心在于

class ThemedButton extends Component {
  // 使用 contextType
  static contextType = ThemeContext
           

当我们为类组件定义静态的上下文类型(

static contextType

)的时候,我们就可以直接透过

this.context

获取上下文中的全局数据对象如下

class ThemedButton extends Component {
  // 使用 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 多个 Context 上下文

然而我们看到前面两个例子中,都只存在一个全局数据对象,那可怎么办。实际上我们可以直接简单嵌套

Context.Provider

就好了:

1.4.1 新的上下文对象 UserContext

首先我们先创建一个新的全局数据

  • src/context/users.js

const users = {
  donovan: {
    name: 'Donovan',
    age: 22,
  },
  alice: {
    name: 'Alice',
    age: 18,
  },
}

export default users
           

接下来创建一个新的上下文对象

  • src/context/UserContext.js

import React from 'react'
import users from './users'

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

1.4.2 嵌套使用 Provider

接下来我们直接将两个

Provider

组件叠加在一起就好了

  • 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 使用不同的 Consumer 接受数据

有了多个 Context 的存在的时候,我们如果使用

contextType

的用法那就只能使用一种 Context 数据类型,因为

contextType

只能有一个类型值咯。

要想一次使用多个全局数据对象的话,就需要回到

Context.Consumer

的用法,不同的 Context 对象提供的 Consumer 就会传入对应的全局数据值,如下:

  • 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>
    </>
  )
}
           

我们可以看到

ThemeContext.Consumer

组件的 Render Props 传入的就是

theme

全局数据;而

UserContext.Consumer

传入的则是

user

全局数据,最终效果如下

React 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render PropsReact 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render Props前言正文结语其他资源

1.5 将状态转换函数也透过 Context 传递

前面我们注意到改变全局数据的方法如

toggleTheme

signIn

都是透过普通数据流 props 一层层传递下来的,其实我们也可以将相关的全局数据更新函数也放入上下文对象当中

  • src/context/Version4.jsx

class Version4 extends Component {
  constructor(props) {
    super(props)

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

    this.state = {
      theme: themes.light,
      // 将状态改变也通过 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
           

这时候中间组件就完全不会出现任何与全局数据相关的部分

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

最后直接透过设置

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 小结:为什么少用 Context?

到此我们已经看过 Context 的各种使用场景和使用方式了,现在我们再回头来谈谈前面说过的:不要为了仅仅只是简化 props 而使用 Context。

Context 确实能够省略透过 props 传递数据的麻烦事,但是使用 Context 实现存在一个致命的缺陷,所有 Consumer / contextType 相关的组件其实是与全局数据强关联的,所以一旦数据改变的话所有依赖于此的组件都会强制更新。

也就是说如果我们把原本需要用 props 传递的共享数据一股脑塞入 Context 当中,会变成如下:

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

看起来好像没问题,但是其实实际上子组件当中不同组件可能仅仅只是依赖于其中一个数据,然而 Context 更新数据是全局的,也就是说当我们更新

props1

的时候,可能造成依赖于

props2、props3

的子组件也都一起重新渲染,造成额外而不必要的渲染浪费性能。

1.6.1 使用规范(推荐)

至此我们已经知晓 Context 的使用模式和缺陷,总归就是一句话:

Context 用于传递真正需要被多个组件共同需要的 全局数据

而当我们仅仅只是需要简化数据在组件树透过 props 一层层传递的麻烦事的话,则应该使用下面一个段落要说明的 组件组合(Component Composition) 的方式。

2. 组件组合(Component Composition)

前面我们提到了,如果我们仅仅只是为了规避层层传递 props 的风险,而不是要使用真正的全局数据的时候,就应该避免使用 Context,而是使用 组件组合 的概念。

2.1 什么叫组件组合?

组件组合的核心思想在于,既然我们不希望共享数据透过 props 一层层传递下去,那么我们就先在顶层将绑定好数据的组件传入 props,而子组件则只需要指定传入的组件真实放置的位置就行

也就是从下面这种形式

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>
}
           

变成这种形式

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>
}
           

甚至进一步的

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

function ComponentB(props) {
    return props.componentC
}
           

如此一来我们就不需要透过 props 传递数据,而是直接传递绑定好数据的组件到指定位置,这就是组件的核心概念。

这种做法相当于是一种 控制反转(Inversion of Controll) 的体现,将子组件的渲染逻辑提升到更高级的组件,反过来由高级组件来提供子组件的实现,而原本的中间组件变成只需要接受父组件传递过来的部件绑定到正确的位置就行

下面我们就来看看组件组合的不同实现方式

2.2 组件组合第一式:渲染子节点数组

首先第一种就是最常见的

children

属性。在 React 中 children 是一个非常特别的属性,当我们使用组件并在组件标签之间放入数据的时候,它其实就会作为 chilren 属性的一员出现,也就是说下面两种实现是等价的

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

// 等价于

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

而当 children 种存在多个元素的时候,他就自然而然变成一个数组,也就是列表渲染的形式,下面就是我们的演示代码

  • 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
           

我们可以看到,示例中我们直接在顶层组件根据数据组装好侧边栏(

SideBar

)需要使用的导航列表

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)}
      />
    ))
           

然后将列表放入到

SideBar

组件的中间,也就是作为

children

属性传入

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

最后在

SideBar

组件内部则是直接放置到目标位置即可

  • 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
           

效果如下

React 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render PropsReact 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render Props前言正文结语其他资源

2.3 组件组合第二式:插槽(slot)

前面我们使用

children

属性来传递子组件,但是它就只是单一的一个属性,不能非常精确的描述子组件的位置。

第二种实现方式则是透过指定名称的 props 传入子组件,进而指定特定子组件对应的位置,而这种实现被称为 插槽(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
           

首先我们先定义一个

Header

组件,并留下

left、right

两个插槽,分别放置于 “Header” 文字的两侧

  • 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>
    )
  }
}
           

接下来我们将

Title

组件传入

left

属性;而将

UserInfo

组件传入

right

属性,这样对于外部组件来说我只要指定传入的属性便等同于传入指定位置,而不用关心具体渲染的位置,也不需要额外的 props 传递,效果如下

React 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render PropsReact 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render Props前言正文结语其他资源

2.4 组件组合第三式:特殊实例

第三种实现则是针对不同实现效果预先提取一些特殊绑定值的实例。

我们在开发组件的时候通常会遵从一个原则:尽量使得最底层的组件越简单愈好,最好只简单依赖于 props 来实现结果渲染,也就是说我们会先定义一个如下的抽象组件:

  • src/composition/Footer.jsx

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

但是每次要使用的时候都要再自己传入属性或是根据更高级的组件传递下来的某个值来渲染组件。事实上,我们还可以预先定义几个传入特定值的组件实例,同时将这些实例也做成另一个组件如下

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

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

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

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

如此一来我们使用的时候就不在需要传入

color

属性进行绑定,而是好像使用静态组件一样,拿来直接用就行了

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

效果如下

React 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render PropsReact 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render Props前言正文结语其他资源

2.5 组件组合第四式:Render Props 传递渲染函数

最后一种实现比较特别,记得我们前面使用 props 传入绑定好属性的的组件时,都是直接传入一个组件实例,然后在子组件中直接将部件放置到固定的位置,这时候我们是不是还可以传入一个函数,使得部件延迟到子组件中再进行绑定呢?下面我们就来试试看

  1. 首先我们先定义一个跟踪鼠标位置的组件
  • 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. 然后直接在父组件中使用
class Main extends Component {
  render() {
    return (
      <div className="main">
        <Mouse />
      </div>
    )
  }
}
           

效果如下

React 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render PropsReact 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render Props前言正文结语其他资源

接下来我们想要渲染一个方块,跟着鼠标一起移动。

首先是方块类

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,
      }}
    />
  )
}
           

接下来我们可能想要直接放到

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>
        <Square position={this.state} />
      </div>
    )
  }
}
           

但是这样有一个问题在于

Mouse

组件就与

Square

组件强耦合了,这是我们不想看到的,当我们需要将

Mouse

组件中的

Square

组件改成其他组件的时候就会遇到大麻烦。

这时候我们就可以用上 Render Props 的概念,透过父组件传入需要的部件(

Square

);更进一步,这个部件依旧能够与子组件(

Mouse

)相关联,这时候我们就不能直接传入组件实例,而是一个 Render Props,也就是一个延迟绑定组件实例的方法如下

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

我们想要在

Mouse

里面渲染

Square

没错,但是又必须等到拿到

Mouse

里面的位置信息才能够进行渲染,所以我们传入一个接受

position

为参数才生成真正的

Square

实例的函数,而在

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>
    )
  }
}
           

我们可以看到,虽然

Mouse

并不知道最终被渲染到该位置的组件是谁,但是我知道只要调用

props.render

方法并传入自己的位置信息,就能够返回一个绑定了自己的鼠标信息的某个组件,这就是 Render Props

效果如下

React 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render PropsReact 高阶指引: Context 上下文 &amp; 组件组合 &amp; Render Props前言正文结语其他资源

注意:这里说 Render Props 的意义只是 “传入某个能生成真正组件实例的函数”,也就是说我们并不是一定要用

render

属性,随便什么属性都好,核心思想就是传入一个根据子组件信息生成真正的部件一个设计模式。

是不是很像前面

Context.Consumer

的用法呢?

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

没错其实

Context.Consumer

就是利用 Render Props 的做法,将全局数据传入用户自定义的

value => Component

函数中,完整对全局数据的绑定的。

结语

本篇介绍了 Context 上下文的用法,还有一些组件组合的使用示例,最后引出 Render Props 的特性和用法,以及在

Context.Consumer

中的具体实现。这些都是实际开发的时候非常实用的特性,供大家参考。

其他资源

参考连接

Title Link
React 官方 - Context https://react.docschina.org/docs/context.html
React 官方 - 组合 vs 继承 https://react.docschina.org/docs/composition-vs-inheritance.html
React 官方 - Render Props https://react.docschina.org/docs/render-props.html
React 中 Context 和 contextType的使用 https://blog.csdn.net/landl_ww/article/details/93514944

完整代码示例

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

继续阅读