什么,还可以通过props传递render函数?

675 阅读5分钟

背景介绍

这是设计模式系列的第十一节,学习的是patterns.dev里设计模式中render函数作为props模式内容,由于是资料是英文版,所以我的学习笔记就带有翻译的性质,但并不是翻译,记录的是自己的学习过程和理解

关于设计模式前九节的内容,在文末会有直达链接。

render函数作为props模式

极简释义:通过props或children传递render函数动态渲染组件。

正文开始

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

高阶组件一章,我们知道通过高阶组件,我们可以多组件复用数据或逻辑,另外一种实现方法就是传递render函数作为Props或children。

传递render函数这种模式就是给组件传递一个渲染函数,这个渲染函数返回JSX组件,而组件自己可以不渲染任何东西,只执行传递的render函数

正如前面给到的示例Title组件传递一个render函数那样。

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")
);

那么我们为什么要这样这样传递render函数呢?这样做的好处就是让Title组件具有很强的复用性,每次都可以传递不同的render,从而执行不同的渲染逻辑

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


const Title = (props) => props.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函数并一定需要命名为render,也可以同时传递多个render函数,同时进行渲染,像下面这样:

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")
);

进阶用法

通常情况下使用render作为props的组件,不仅执行传递过来的render渲染JSX,还会有一些数据和逻辑处理

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

  return props.render(data)
}

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

比如说上面这个例子,render函数还接受Component传递过来的data数据,然后再把数据转交给ChildComponet进行渲染。

案例分析

接下来,我们来看一个小小的案例:一个小应用,用户可以输入一个摄氏温度,我们将摄氏温度转换里氏标准温度,代码如下:

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

上面的代码就遇到一个问题,输入框组件里的state温度值,同级的KelvinFahrenheit组件拿不到值

提升state层级

这时有一个常见的办法是,提升state层级到APP,传递state和setState方法给input输入框:

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

虽然这是个可行的方法,但是在复杂的大型应用中,提升state的层级是一件危险的操作,而高层级的state变化会让该层级的所有子组件重新渲染,而那些不使用该state的组件就会过度渲染,从而影响性能

有没有什么办法不提升state层级呢?这个时候就适合render作为props这种模式上场了

render函数作为props模式

由前文介绍,我们可以render函数作为props传递给组件。那么在这个案例中,我们是不是可以把 Kelvin和Fahrenheit这两个组件作为render传递给Input组件呢?

答案是当然的,这样就可以提升state层级,同时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>
  );
}

用render函数作为children

还有一种大家更常见的写法是把render函数作为children传递给组件,记得我在使用ant的组件和react-dnd时,就见过这种写法。

比如上面的例子,我们在Input组件的children里写render函数,在Input组件里调用children函数进行渲染

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

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

总结

通过props或children传递render函数这种模式,和高阶组件模式一样,都解决多组件复用数据和逻辑问题,当然也包括增强组件复用性。并且在一些场景下,高阶组件可以替代传递render函数。但是相较于高阶组件模式,传递render函数模式有自己的优点

  • 传递render函数渲染这种模式,直接传递props,没有自动合并props,所以没有高阶组件的命名冲突覆盖问题;
  • 数据更容易追溯源头,没有高阶组件的隐藏props的问题;
  • 实现逻辑和视图渲染的分离

当然传递render函数这种模式,在大部分场景下,可以被Hooks取代。当然如果在render函数里没有使用生命周期函数,并且不改变接受到的state,那么使用传递render函数模式也是很好的。

相关推荐

第一节:单例模式:高并发造成的数据统计困难?看我单例模式一招制敌

第二节:替身模式:JS和迪丽热巴一样有专业替身?没听过的快来补补课...

第三节:供应商模式:还在层层传递props?来学学非常实用的供应商模式吧

第四节:原型模式:都知道JavaScript原型,但设计模式里的原型模式你会用吗?

第五节:视图和逻辑分离模式:React Hooks时代,怎么实现视图与逻辑分离呢?

第六节:观察者模式:是时候拿出高级的技术了————观察者模式

第七节:模块化模式:前端性能优化进阶篇——动态加载模块基础补遗

第八节:混合模式:在React Hook时代,Object.assign这种混合写法还要用吗?

第九节:中间件模式:如何使用中间件优化多对多通信?

第十节:高阶组件模式:在React Hooks时代,高阶组件只能感叹:既生瑜何生亮?

相关活动

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情