render 道具模式(X)

这是历史遗留问题,所以不建议使用(现在有了Hooks),在这里只是让我们了解这段历史

通过 props 道具将 JSX 元素传递给组件

在高阶组件部分,我们看到如果多个组件需要访问相同的数据或包含相同的逻辑,那么能够重用组件逻辑会非常方便。

另一种使组件非常可重用的方法是使用 render 道具模式。
render 道具是组件上的道具,其值是一个返回 JSX 元素的函数。
除了 render 道具之外,组件本身不会渲染任何东西。 相反,组件只是调用 render 道具,而不是实现自己的渲染逻辑。


render 道具

假设有一个 Title 组件。
在这种情况下,Title 组件除了渲染传递的值之外不应该做任何事情。 我们可以为此使用 render 道具!
让我们将 Title 组件渲染的值传递给 render 道具。

<Title render={() => <h1>I am a render prop!</h1>} />

在 Title 组件中,可以通过返回调用的 render 道具来渲染这些数据!

const Title = props => props.render();

对于 Component 元素,我们必须传递一个名为 render 的道具,它是一个返回 React 元素的函数。

import React from "react";
import { render } from "react-dom";

import "./styles.css";

const Title = (props) => props.render();

render(
  <div className="App">
    <Title
      render={() => (
        <h1>
          <span role="img" aria-label="emoji">
            ✨
          </span>
          I am a render prop!{" "}
          <span role="img" aria-label="emoji">
            ✨
          </span>
        </h1>
      )}
    />
  </div>,
  document.getElementById("root")
);

Perfect,工作顺利! render 道具很酷的一点是,接收道具的组件是非常可重用的。

我们可以多次使用它,每次将不同的值传递给 render 道具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

import React from "react";
import { render } from "react-dom";
import "./styles.css";

const Title = (props) => props.render();

render(//注意,这个才是本文所说的 render 道具
  <div className="App">
    <Title render={() => <h1> First render prop! </h1>} />
    <Title render={() => <h2>🔥 Second render prop! 🔥</h2>} />
    <Title render={() => <h3>🚀 Third render prop! 🚀</h3>} />
  </div>,
  document.getElementById("root")
);

虽然它们被称为 render 道具,但上面Title中的 render 道具不一定要被称为 render。(注意和本文所述的真正的 render 道具的区别)
任何渲染 JSX 的道具都被视为渲染道具! 让我们重命名上一个示例中使用的渲染道具,并为它们指定特定的名称!

import React from "react";
import { render } from "react-dom";
import "./styles.css";

const Title = (props) => (
  <>
    {props.renderFirstComponent()}
    {props.renderSecondComponent()}
    {props.renderThirdComponent()}
  </>
);

render(
  <div className="App">
    <Title
      renderFirstComponent={() => <h1>✨ First render prop! ✨</h1>}
      renderSecondComponent={() => <h2>🔥 Second render prop! 🔥</h2>}
      renderThirdComponent={() => <h3>🚀 Third render prop! 🚀</h3>}
    />
  </div>,
  document.getElementById("root")
);

Great! 我们刚刚看到可以使用 render 道具来使组件可重用,因为我们每次都可以将不同的数据传递给 render 道具。

但是,为什么要使用这个?

一个接受 render 道具的组件通常不仅仅是简单地调用 render 道具。
相反,我们通常希望将数据从接受 render 道具的组件传递到作为 render 道具传递的元素!

function Component(props) {
  const data = { ... }

  return props.render(data)
}

render 道具现在可以接收作为参数传递的这个值。

<Component render={data => <ChildComponent data={data} />}

案例分析

让我们看一个例子!
有一个简单的应用程序,用户可以在其中输入摄氏温度。 该应用程序以华氏度和开尔文显示该温度的值。

import React, { useState } from "react";
import "./styles.css";

function Input() {
  const [value, setValue] = useState("");

  return (
    <input
      type="text"
      value={value}
      onChange={e => setValue(e.target.value)}
      placeholder="Temp in °C"
    />
  );
}

export default function App() {
  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input />
      <Kelvin />
      <Fahrenheit />
    </div>
  );
}

function Kelvin({ value = 0 }) {
  return <div className="temp">{value + 273.15}K</div>;
}

function Fahrenheit({ value = 0 }) {
  return <div className="temp">{(value * 9) / 5 + 32}°F</div>;
}

嗯.. 目前有问题。
有状态的 Input 组件包含用户输入的值,这意味着 Fahrenheit 和 Kelvin 组件无权访问用户的输入!

- - - 吊起状态

在上面的示例中,使用户输入可用于华氏度和开尔文分量的一种方法是,解除状态。

我们有一个有状态的 Input 组件。 但是,兄弟组件 Fahrenheit 和 Kelvin 也需要访问这些数据。
可以将状态提升到与 Input、Fahrenheit 和 Kelvin 有连接的第一个公共祖先组件,而不是有状态的 Input 组件:在这种情况下是 App 组件!

function Input({ value, handleChange }) {
  return <input value={value} onChange={e => handleChange(e.target.value)} />;
}

export default function App() {
  const [value, setValue] = useState("");

  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input value={value} handleChange={setValue} />
      <Kelvin value={value} />
      <Fahrenheit value={value} />
    </div>
  );
}

尽管这是一个有效的解决方案,但在具有处理许多子级的组件的大型应用程序中提升状态可能会很棘手。
每个状态更改都可能导致重新渲染所有子项,即使是那些不处理数据的子项,这可能会对您的应用程序的性能产生负面影响。

- - - render 道具

相反,我们可以使用 render 道具!
让我们以一种可以接收 render 道具的方式更改 Input 组件。

function Input(props) {
  const [value, setValue] = useState("");

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="Temp in °C"
      />
      {props.render(value)}
    </>
  );
}

export default function App() {
  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input
        render={value => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      />
    </div>
  );
}

Perfect, Kelvin 和 Fahrenheit 现在可以访问用户输入的值!


作为函数的 children

除了常规的 JSX 组件,还可以将函数作为子组件传递给 React 组件。
可以通过 children 道具使用这个功能,它在技术上也是一个 render 道具。

让我们更改输入组件。
我们不会显式地传递 render 道具,而是将一个函数作为 Input 组件的子组件传递。

export default function App() {
  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input>
        {value => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      </Input>
    </div>
  );
}

我们可以通过 Input 组件上可用的 props.children 属性访问这个函数。 我们将使用用户输入的值调用 props.children,而不是使用用户输入的值调用 props.render。

function Input(props) {
  const [value, setValue] = useState("");

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="Temp in °C"
      />
      {props.children(value)}
    </>
  );
}

太好了,这样 Kelvin 和 Fahrenheit 组件可以访问该值,而不必担心 render 道具的名称。


Hooks

在某些情况下,可以用 Hooks 替换渲染道具。 Apollo Client 就是一个很好的例子。

无需使用 Apollo Client 的经验即可理解此示例。

使用 Apollo Client 的一种方法是通过 Mutation 和 Query 组件。

让我们看一下高阶组件部分中介绍的相同输入示例。
现在将使用接收 render 道具的 Mutation 组件,而不是使用 graphql() 高阶组件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

//App.js
import React from "react";
import "./styles.css";

import InputRenderProp from "./InputRenderProp";
import InputHooks from "./InputHooks";

export default function App() {
  return (
    <div className="App">
      <div className="col">
        <h3>Render Prop</h3>
        <InputRenderProp />
      </div>
      <div className="col">
        <h3>Hooks</h3>
        <InputHooks />
      </div>
    </div>
  );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

//resolvers.js
import { gql } from "apollo-boost";

export const ADD_MESSAGE = gql`
  mutation AddMessage($message: String!) {
    addMessage(message: $message) {
      message
    }
  }
`;
 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

//InputRenderProp.js
import React from "react";
import "./styles.css";

import { Mutation } from "react-apollo";
import { ADD_MESSAGE } from "./resolvers";

export default class Input extends React.Component {
  constructor() {
    super();
    this.state = { message: "" };
  }

  handleChange = (e) => {
    this.setState({ message: e.target.value });
  };

  render() {
    return (
      <Mutation
        mutation={ADD_MESSAGE}
        variables={{ message: this.state.message }}
        onCompleted={() =>
          console.log(`Added with render prop: ${this.state.message} `)
        }
      >
        {(addMessage) => (
          <div className="input-row">
            <input
              onChange={this.handleChange}
              type="text"
              placeholder="Type something..."
            />
            <button onClick={addMessage}>Add</button>
          </div>
        )}
      </Mutation>
    );
  }
}
 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

//InputHooks.js
import React, { useState } from "react";
import "./styles.css";

import { useMutation } from "@apollo/react-hooks";
import { ADD_MESSAGE } from "./resolvers";

export default function Input() {
  const [message, setMessage] = useState("");
  const [addMessage] = useMutation(ADD_MESSAGE, {
    variables: { message },
    onCompleted: () => console.log(`Added with hook: ${message} `)
  });

  return (
    <div className="input-row">
      <input
        onChange={(e) => setMessage(e.target.value)}
        type="text"
        placeholder="Type something..."
      />
      <button onClick={addMessage}>Add</button>
    </div>
  );
}

为了将数据从 Mutation 组件向下传递到需要数据的元素,我们将一个函数作为子项传递。 该函数通过其参数接收数据的值。

<Mutation mutation={...} variables={...}>
  {addMessage => <div className="input-row">...</div>}
</Mutation>

尽管我们仍然可以使用 render 道具模式并且与高阶组件模式相比通常更受欢迎,但它有其缺点。

缺点之一是组件嵌套较深。 如果一个组件需要访问多个突变或查询,我们可以嵌套多个 Mutation 或 Query 组件。

<Mutation mutation={FIRST_MUTATION}>
  {firstMutation => (
    <Mutation mutation={SECOND_MUTATION}>
      {secondMutation => (
        <Mutation mutation={THIRD_MUTATION}>
          {thirdMutation => (
            <Element
              firstMutation={firstMutation}
              secondMutation={secondMutation}
              thirdMutation={thirdMutation}
            />
          )}
        </Mutation>
      )}
    </Mutation>
  )}
</Mutation>

在 Hooks 发布后,Apollo 为 Apollo Client 库添加了 Hooks 支持。
开发人员现在可以通过库提供的钩子直接访问数据,而不是使用 Mutation 和 Query 渲染道具。

让我们看一个示例,该示例使用与我们之前在带有 Query 渲染道具的示例中看到的完全相同的数据。
这一次,使用 Apollo Client 为我们提供的 useQuery 钩子向组件提供数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

//App.js
import React from "react";
import "./styles.css";

import InputHOC from "./InputHOC";
import InputHooks from "./InputHooks";

export default function App() {
  return (
    <div className="App">
      <div className="col">
        <h3>HOC</h3>
        <InputHOC />
      </div>
      <div className="col">
        <h3>Hooks</h3>
        <InputHooks />
      </div>
    </div>
  );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

//resolvers.js
import { gql } from "apollo-boost";

export const ADD_MESSAGE = gql`
  mutation AddMessage($message: String!) {
    addMessage(message: $message) {
      message
    }
  }
`;
 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

//InputHOC.js
import React from "react";
import "./styles.css";

import { graphql } from "react-apollo";
import { ADD_MESSAGE } from "./resolvers";

class Input extends React.Component {
  constructor() {
    super();
    this.state = { message: "" };
  }

  handleChange = (e) => {
    this.setState({ message: e.target.value });
  };

  handleClick = () => {
    this.props.mutate({ variables: { message: this.state.message } });
  };

  render() {
    return (
      <div className="input-row">
        <input
          onChange={this.handleChange}
          type="text"
          placeholder="Type something..."
        />
        <button onClick={this.handleClick}>Add</button>
      </div>
    );
  }
}

export default graphql(ADD_MESSAGE)(Input);
 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

//InputHooks.js
import React, { useState } from "react";
import "./styles.css";

import { useMutation } from "@apollo/react-hooks";
import { ADD_MESSAGE } from "./resolvers";

export default function Input() {
  const [message, setMessage] = useState("");
  const [addMessage] = useMutation(ADD_MESSAGE, {
    variables: { message }
  });

  return (
    <div className="input-row">
      <input
        onChange={(e) => setMessage(e.target.value)}
        type="text"
        placeholder="Type something..."
      />
      <button onClick={addMessage}>Add</button>
    </div>
  );
}

通过使用 useQuery 钩子,我们减少了为组件提供数据所需的代码量。


优点

使用渲染道具模式可以轻松地在多个组件之间共享逻辑和数据。通过使用 render 或 children 道具,组件可以变得非常可重用。
虽然高阶组件模式主要解决相同的问题,即可重用性和共享数据,但渲染道具模式解决了我们使用 HOC 模式可能遇到的一些问题。

使用 HOC 模式可能遇到的命名冲突问题不再适用于使用渲染道具模式,因为我们不会自动合并道具。
我们使用父组件提供的值显式地将 props 传递给子组件。

由于我们显式传递 props,我们解决了 HOC 的隐式 props 问题。应该传递给元素的道具在渲染道具的参数列表中都是可见的。
这样,我们确切地知道某些道具来自哪里。

我们可以通过渲染道具将应用程序的逻辑与渲染组件分开。接收渲染道具的有状态组件可以将数据传递给无状态组件,无状态组件仅渲染数据。


缺点

我们试图用渲染道具解决的问题在很大程度上已被 React Hooks 取代。 随着 Hooks 改变了我们为组件添加可重用性和数据共享的方式,它们可以在许多情况下取代渲染道具模式。

由于我们无法向 render prop 添加生命周期方法,因此我们只能在不需要更改它们接收到的数据的组件上使用它。


知识点

  • render
  • props
  • children