天天看點

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

繼續閱讀