设计模式
渲染道具模式
在关于 高阶组件 的部分,我们看到,如果多个组件需要访问相同的数据,或包含相同的逻辑,那么能够重用组件逻辑会非常方便。
另一种使组件高度可重用的方法是使用 渲染道具 模式。渲染道具是组件上的一个道具,其值是一个返回 JSX 元素的函数。组件本身除了渲染道具之外不会渲染任何内容。相反,组件只是调用渲染道具,而不是实现自己的渲染逻辑。
假设我们有一个 Title
组件。在这种情况下,Title
组件除了渲染我们传递的值之外不应该做任何事情。我们可以为此使用渲染道具!让我们将要渲染的 Title
组件的值传递给 render
道具。
<Title render={() => <h1>I am a render prop!</h1>} />
在 Title
组件中,我们可以通过返回调用的 render
道具来渲染此数据!
const Title = (props) => props.render();
对于 Component
元素,我们必须传递一个名为 render
的道具,它是一个返回 React 元素的函数。
1import React from "react";2import { render } from "react-dom";34import "./styles.css";56const Title = (props) => props.render();78render(9 <div className="App">10 <Title11 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
道具传递不同的值。
1import React from "react";2import { render } from "react-dom";3import "./styles.css";45const Title = (props) => props.render();67render(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 的道具都被认为是渲染道具!让我们重命名在前面示例中使用的渲染道具,并改为使用特定的名称!
1import React from "react";2import { render } from "react-dom";3import "./styles.css";45const Title = (props) => (6 <>7 {props.renderFirstComponent()}8 {props.renderSecondComponent()}9 {props.renderThirdComponent()}10 </>11);1213render(14 <div className="App">15 <Title16 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} />}
让我们看一个例子!我们有一个简单的应用程序,用户可以在其中输入摄氏温度。该应用程序显示此温度的值(以华氏度和开尔文表示)。
1import React, { useState } from "react";2import "./styles.css";34function Input() {5 const [value, setValue] = useState("");67 return (8 <input9 type="text"10 value={value}11 onChange={e => setValue(e.target.value)}12 placeholder="Temp in °C"13 />14 );15}1617export default function App() {18 return (19 <div className="App">20 <h1>☃️ Temperature Converter 🌞</h1>21 <Input />22 <Kelvin />23 <Fahrenheit />24 </div>25 );26}2728function Kelvin({ value = 0 }) {29 return <div className="temp">{value + 273.15}K</div>;30}3132function Fahrenheit({ value = 0 }) {33 return <div className="temp">{(value * 9) / 5 + 32}°F</div>;34}
嗯.. 目前存在一个问题。有状态的 Input
组件包含用户输入的值,这意味着 Fahrenheit
和 Kelvin
组件无法访问用户的输入!
提升状态
在上面的示例中,使用户输入可供 Fahrenheit
和 Kelvin
组件访问的一种方法是提升状态。
在这种情况下,我们有一个有状态的 Input
组件。但是,兄弟组件 Fahrenheit
和 Kelvin
也需要访问此数据。与其使用有状态的 Input
组件,不如将状态提升到与 Input
、Fahrenheit
和 Kelvin
相连的第一个共同祖先组件:在这种情况下为 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>
);
}
完美,Kelvin
和 Fahrenheit
组件现在可以访问用户输入的值!
1import React, { useState } from "react";2import "./styles.css";34function Input(props) {5 const [value, setValue] = useState("");67 return (8 <>9 <input10 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}1920export default function App() {21 return (22 <div className="App">23 <h1>☃️ Temperature Converter 🌞</h1>24 <Input25 render={value => (26 <>27 <Kelvin value={value} />28 <Fahrenheit value={value} />29 </>30 )}31 />32 </div>33 );34}3536function Kelvin({ value }) {37 return <div className="temp">{parseInt(value || 0) + 273.15}K</div>;38}3940function 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)}
</>
);
}
很好,这样一来,Kelvin
和 Fahrenheit
组件就可以访问该值,而不必担心 render
道具的名称。
1import React, { useState } from "react";2import "./styles.css";34function Input(props) {5 const [value, setValue] = useState(0);67 return (8 <>9 <input10 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}1920export 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}3536function Kelvin({ value }) {37 return <div className="temp">{parseInt(value || 0) + 273.15}K</div>;38}3940function Fahrenheit({ value }) {41 return <div className="temp">{(parseInt(value || 0) * 9) / 5 + 32}°F</div>;42}
Hooks
在某些情况下,我们可以用 Hooks 替换渲染道具。一个很好的例子是 Apollo Client。
无需了解 Apollo Client 即可理解此示例。
使用 Apollo Client 的一种方法是通过 Mutation
和 Query
组件。让我们看一下我们在高阶组件部分中介绍的相同的 Input
示例。这次,我们将使用接收渲染道具的 Mutation
组件,而不是使用 graphql()
高阶组件。
1import React from "react";2import "./styles.css";34import { Mutation } from "react-apollo";5import { ADD_MESSAGE } from "./resolvers";67export default class Input extends React.Component {8 constructor() {9 super();10 this.state = { message: "" };11 }1213 handleChange = (e) => {14 this.setState({ message: e.target.value });15 };1617 render() {18 return (19 <Mutation20 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 <input29 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>
虽然我们仍然可以使用渲染道具模式,并且它通常优于高阶组件模式,但它也有一些缺点。
缺点之一是组件嵌套过深。如果组件需要访问多个变异或查询,我们可以嵌套多个 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
钩子来向组件提供数据。
1import React, { useState } from "react";2import "./styles.css";34import { useMutation } from "@apollo/react-hooks";5import { ADD_MESSAGE } from "./resolvers";67export default function Input() {8 const [message, setMessage] = useState("");9 const [addMessage] = useMutation(ADD_MESSAGE, {10 variables: { message }11 });1213 return (14 <div className="input-row">15 <input16 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
道具添加生命周期方法,因此我们只能在不需要更改接收到的数据的组件上使用它。