对我们的下一本书感兴趣吗?了解更多关于 使用 React 构建大型 JavaScript Web 应用程序

设计模式

渲染道具模式

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

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

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

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

Title 组件中,我们可以通过返回调用的 render 道具来渲染此数据!

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

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

index.js
1import React from "react";
2import { render } from "react-dom";
3
4import "./styles.css";
5
6const Title = (props) => props.render();
7
8render(
9 <div className="App">
10 <Title
11 render={() => (
12 <h1>
13 <span role="img" aria-label="emoji">
14
15 </span>
16 I am a render prop!{" "}
17 <span role="img" aria-label="emoji">
18
19 </span>
20 </h1>
21 )}
22 />
23 </div>,
24 document.getElementById("root")
25);

完美,运行流畅!渲染道具的妙处在于接收道具的组件非常可重用。我们可以多次使用它,每次向 render 道具传递不同的值。

index.js
1import React from "react";
2import { render } from "react-dom";
3import "./styles.css";
4
5const Title = (props) => props.render();
6
7render(
8 <div className="App">
9 <Title render={() => <h1>✨ First render prop! ✨</h1>} />
10 <Title render={() => <h2>🔥 Second render prop! 🔥</h2>} />
11 <Title render={() => <h3>🚀 Third render prop! 🚀</h3>} />
12 </div>,
13 document.getElementById("root")
14);

虽然它们被称为 render 道具,但渲染道具不必称为 render。任何渲染 JSX 的道具都被认为是渲染道具!让我们重命名在前面示例中使用的渲染道具,并改为使用特定的名称!

index.js
1import React from "react";
2import { render } from "react-dom";
3import "./styles.css";
4
5const Title = (props) => (
6 <>
7 {props.renderFirstComponent()}
8 {props.renderSecondComponent()}
9 {props.renderThirdComponent()}
10 </>
11);
12
13render(
14 <div className="App">
15 <Title
16 renderFirstComponent={() => <h1>✨ First render prop! ✨</h1>}
17 renderSecondComponent={() => <h2>🔥 Second render prop! 🔥</h2>}
18 renderThirdComponent={() => <h3>🚀 Third render prop! 🚀</h3>}
19 />
20 </div>,
21 document.getElementById("root")
22);

很好!我们刚刚看到,我们可以使用渲染道具来使组件可重用,因为我们可以每次向渲染道具传递不同的数据。但是,为什么要使用它呢?

一个接收渲染道具的组件通常比仅仅调用 render 道具做更多的事情。相反,我们通常希望将数据从接收渲染道具的组件传递到我们作为渲染道具传递的元素!

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

  return props.render(data)
}

渲染道具现在可以接收我们作为参数传递的这个值。

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

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

App.js
1import React, { useState } from "react";
2import "./styles.css";
3
4function Input() {
5 const [value, setValue] = useState("");
6
7 return (
8 <input
9 type="text"
10 value={value}
11 onChange={e => setValue(e.target.value)}
12 placeholder="Temp in °C"
13 />
14 );
15}
16
17export default function App() {
18 return (
19 <div className="App">
20 <h1>☃️ Temperature Converter 🌞</h1>
21 <Input />
22 <Kelvin />
23 <Fahrenheit />
24 </div>
25 );
26}
27
28function Kelvin({ value = 0 }) {
29 return <div className="temp">{value + 273.15}K</div>;
30}
31
32function Fahrenheit({ value = 0 }) {
33 return <div className="temp">{(value * 9) / 5 + 32}°F</div>;
34}

嗯.. 目前存在一个问题。有状态的 Input 组件包含用户输入的值,这意味着 FahrenheitKelvin 组件无法访问用户的输入!


提升状态

在上面的示例中,使用户输入可供 FahrenheitKelvin 组件访问的一种方法是提升状态。

在这种情况下,我们有一个有状态的 Input 组件。但是,兄弟组件 FahrenheitKelvin 也需要访问此数据。与其使用有状态的 Input 组件,不如将状态提升到与 InputFahrenheitKelvin 相连的第一个共同祖先组件:在这种情况下为 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>
  );
}

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


渲染道具

相反,我们可以使用渲染道具!让我们以一种可以接收渲染道具的方式更改 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>
  );
}

完美,KelvinFahrenheit 组件现在可以访问用户输入的值!

App.js
1import React, { useState } from "react";
2import "./styles.css";
3
4function Input(props) {
5 const [value, setValue] = useState("");
6
7 return (
8 <>
9 <input
10 type="text"
11 value={value}
12 onChange={e => setValue(e.target.value)}
13 placeholder="Temp in °C"
14 />
15 {props.render(value)}
16 </>
17 );
18}
19
20export default function App() {
21 return (
22 <div className="App">
23 <h1>☃️ Temperature Converter 🌞</h1>
24 <Input
25 render={value => (
26 <>
27 <Kelvin value={value} />
28 <Fahrenheit value={value} />
29 </>
30 )}
31 />
32 </div>
33 );
34}
35
36function Kelvin({ value }) {
37 return <div className="temp">{parseInt(value || 0) + 273.15}K</div>;
38}
39
40function Fahrenheit({ value }) {
41 return <div className="temp">{(parseInt(value || 0) * 9) / 5 + 32}°F</div>;
42}

子组件作为函数

除了常规的 JSX 组件之外,我们还可以将函数作为子组件传递给 React 组件。此函数通过 children 道具提供给我们,从技术上讲,它也是一个渲染道具。

让我们更改 Input 组件。与其显式地传递 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.render,不如使用用户输入的值调用 props.children

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

很好,这样一来,KelvinFahrenheit 组件就可以访问该值,而不必担心 render 道具的名称。

App.js
1import React, { useState } from "react";
2import "./styles.css";
3
4function Input(props) {
5 const [value, setValue] = useState(0);
6
7 return (
8 <>
9 <input
10 type="number"
11 value={value}
12 onChange={e => setValue(e.target.value)}
13 placeholder="Temp in °C"
14 />
15 {props.children(value)}
16 </>
17 );
18}
19
20export default function App() {
21 return (
22 <div className="App">
23 <h1>☃️ Temperature Converter 🌞</h1>
24 <Input>
25 {value => (
26 <>
27 <Kelvin value={value} />
28 <Fahrenheit value={value} />
29 </>
30 )}
31 </Input>
32 </div>
33 );
34}
35
36function Kelvin({ value }) {
37 return <div className="temp">{parseInt(value || 0) + 273.15}K</div>;
38}
39
40function Fahrenheit({ value }) {
41 return <div className="temp">{(parseInt(value || 0) * 9) / 5 + 32}°F</div>;
42}

Hooks

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

无需了解 Apollo Client 即可理解此示例。

使用 Apollo Client 的一种方法是通过 MutationQuery 组件。让我们看一下我们在高阶组件部分中介绍的相同的 Input 示例。这次,我们将使用接收渲染道具的 Mutation 组件,而不是使用 graphql() 高阶组件。

InputRenderProp.js
1import React from "react";
2import "./styles.css";
3
4import { Mutation } from "react-apollo";
5import { ADD_MESSAGE } from "./resolvers";
6
7export default class Input extends React.Component {
8 constructor() {
9 super();
10 this.state = { message: "" };
11 }
12
13 handleChange = (e) => {
14 this.setState({ message: e.target.value });
15 };
16
17 render() {
18 return (
19 <Mutation
20 mutation={ADD_MESSAGE}
21 variables={{ message: this.state.message }}
22 onCompleted={() =>
23 console.log(`Added with render prop: ${this.state.message} `)
24 }
25 >
26 {(addMessage) => (
27 <div className="input-row">
28 <input
29 onChange={this.handleChange}
30 type="text"
31 placeholder="Type something..."
32 />
33 <button onClick={addMessage}>Add</button>
34 </div>
35 )}
36 </Mutation>
37 );
38 }
39}

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

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

虽然我们仍然可以使用渲染道具模式,并且它通常优于高阶组件模式,但它也有一些缺点。

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

<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 支持。开发人员现在可以使用库提供的钩子直接访问数据,而不是使用 MutationQuery 渲染道具。

让我们看一个使用与我们在前面使用 Query 渲染道具的示例中相同数据的示例。这次,我们将通过使用 Apollo Client 为我们提供的 useQuery 钩子来向组件提供数据。

InputHOC.js
InputHooks.js
1import React, { useState } from "react";
2import "./styles.css";
3
4import { useMutation } from "@apollo/react-hooks";
5import { ADD_MESSAGE } from "./resolvers";
6
7export default function Input() {
8 const [message, setMessage] = useState("");
9 const [addMessage] = useMutation(ADD_MESSAGE, {
10 variables: { message }
11 });
12
13 return (
14 <div className="input-row">
15 <input
16 onChange={(e) => setMessage(e.target.value)}
17 type="text"
18 placeholder="Type something..."
19 />
20 <button onClick={addMessage}>Add</button>
21 </div>
22 );
23}

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


优点

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

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

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

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


缺点

我们试图用渲染道具解决的问题,在很大程度上已经被 React Hooks 替换了。由于 Hooks 改变了我们向组件添加可重用性和数据共享的方式,因此它们可以在许多情况下替换渲染道具模式。

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


参考资料