React.js 令人惊叹的渲染属性模式 — — 生命周期消失了!

2025-06-07

React.js 令人惊叹的渲染属性模式 — — 生命周期消失了!

在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris

什么是 Render props 模式?Render props 模式是一种创建组件并向子组件提供某种数据的方法。

我们为什么要这么做?假设我们想做以下事情:

  • 获取数据,如果有一个组件可以抽象出所有混乱的 HTTP 并在完成后为您提供数据,那不是很好吗?
  • A/B 测试,当您将应用程序投入生产时,您最终会想要改进,但您可能不知道最好的前进方式,或者您可能想要经常发布并将代码推送到生产中,但某些功能尚未准备好面世,所以您希望能够有条件地决定某些东西是否可见。

替代文本

如果您遇到上述任何一种情况,那么您就拥有可重用的功能。对于可重用的功能,您很可能希望将其抽象为函数或组件,我们将选择后者。

如果我们可以创建组件来实现这个功能,然后直接把它提供给某个组件,那不是很好吗?那个子组件根本不知道它正在被提供数据。

从某种意义上说,这类似于我们对 Provider 的处理方式,但也类似于容器组件如何包装展示组件。这听起来有点模糊,所以我们用一些标记来展示一下它的样子:

const ProductDetail = ({ product }) => ( 
  <React.Fragment> 
    <h2>{product.title}</h2> 
    <div>{product.description}</div> 
  </React.Fragment> ) 

<Fetch url="some url where my data is" 
  render={(data) => <ProductDetail product={data.product} /> }
/>

正如我们上面看到的,我们有两个不同的组件ProductDetail一个看起来像是一个演示组件。Fetch另一个看起来有点不同。它有一个 url 属性,并且似乎有一个 render 属性,最终会渲染我们的ProductDetailFetchProductDetail

渲染 props 解释

我们可以对此进行逆向工程并弄清楚其工作原理。

我们再看一下代码:

<Fetch url="some url where my data is" 
  render={(data) => <ProductDetail product={data.product} /> }
/>

我们的 Fetch 组件有一个属性render,它似乎接受一个函数作为参数,最终生成 JSX 代码。事情是这样的,整个 render-props 模式就是在返回方法中调用一个函数。让我通过一些代码来解释一下:

class Fetch extends React.Component { 
  render() { 
    return this.props.render(); 
  } 
}

这就是这个模式最简单的形式。我们使用Fetch组件的方式意味着我们至少需要在this.props.render()调用中传递一些东西。我们先提取上面的函数调用部分并查看一下:

(data) => <ProductDetail product={data.product} />

上面我们看到我们需要一个参数 data,而 data 似乎是一个对象。那么,data 从何而来呢?这就是我们Fetch组件的职责所在,它为我们做了一些繁重的工作,即执行 HTTP 调用。

为 HTTP 创建组件

让我们添加一些生命周期方法,使其Fetch看起来像这样:

class Fetch extends React.Component { 
  state = { 
    data: void 0, 
    error: void 0 
  } 
  componentDidMount() { 
    this.fetchData(); 
  } 
  async fetchData() { 
    try { 
      const response = await fetch(this.props.url); 
      const json = await response.json(); 
      this.setState({ data: json }); 
    catch (err) { 
      this.setState({ error: err }) 
    } 
  } 
  render() { 
    if (!this.state.data) return null; 
    else return this.props.render(this.state.data); 
  } 
}

快速提示,如果你不熟悉void 0,那只是设置一些东西undefined

好的,现在我们已经稍微充实了我们的组件。我们添加了发出fetchData()HTTP 调用的方法,this.props.url并且可以看到,render()如果未设置,则该方法会呈现 null this.state.data;但如果 HTTP 调用完成,则会this.props.render(data)使用 JSON 响应进行调用。

然而,它缺少三样东西:

  • 处理错误,我们应该添加逻辑来处理错误
  • 处理加载,目前如果 thefetch() 调用尚未完成,我们就不会渲染任何内容,这不是很好
  • 处理 this.props.url,这个 prop 可能最初没有设置,并且可能会随着时间而改变,所以我们应该处理它

处理错误

我们可以通过稍微改变我们的方法来轻松处理这个问题render(),以适应ifthis.state.error设置,毕竟我们已经在方法this.state.error中的catch子句中编写了设置的逻辑fetchData()

如下:

class Fetch extends React.Component { 
  state = { 
    data: void 0, 
    error: void 0 
  } 
  componentDidMount() { 
    this.fetchData(); 
  } 
  async fetchData() { 
    try { 
      const response = await fetch(this.props.url); 
      const json = await response.json(); 
      this.setState({ data: json }); 
    catch (err) { 
      this.setState({ error: err }) 
    } 
  } 
  render() { 
    const { error, data, loading } = this.state; 
    if(error) return this.props.error(error); 
    if (data) return this.props.render(data); 
    else return null; 
  } 
}

上面我们通过调用添加了对 this.state.error 的处理this.props.error(),因此当我们尝试使用该组件时,我们需要反映这一点Fetch

处理装载

对于这个,我们只需要添加一个新的状态加载并更新render()方法来查看所述属性,如下所示:

class Fetch extends React.Component { 
  state = { 
    data: void 0, 
    error: void 0,
    loading: false 
  } 
  componentDidMount() { 
    this.fetchData(); 
  } 
  async fetchData() { 
    try { 
      this.setState({ loading: true }); 
      const response = await fetch(this.props.url); 
      const json = await response.json(); 
      this.setState({ data: json }); 
      this.setState({ loading: false }); 
    catch (err) { 
      this.setState({ error: err }) 
    } 
  }

  render() { 
    const { error, data, loading } = this.state; 
    if(loading) return <div>Loading...</div> 
    if(error) return this.props.error(error); 
    if (data) return this.props.render(data);
    else return null; 
  } 
}

现在,我们在处理加载时有点马虎,是的,我们if为它添加了一个,但我们渲染的内容很可能通过使用看起来像旋转器或重影图像的漂亮组件来改进,所以这值得考虑。

处理 this.props.url 的更改

这个 URL 完全有可能发生变化,我们需要适应它,除非我们计划像这样使用组件

在这种情况下,您应该跳过本节并查看下一部分;)

React API 最近发生了变化,在变化之前,我们需要添加生命周期方法componentWillReceiveProps()来查看 prop 是否发生了变化,但这被认为是不安全的,所以我们必须使用

componentDidUpdate(prevProps) { 
  if (this.props.url && this.props.url !== prevProps.url){
    this.fetchData(this.props.url); 
  } 
}

就是这样,这就是我们需要的,让我们展示这个组件的完整代码:

class Fetch extends React.Component { 
  state = { 
    data: void 0, 
    error: void 0,
    loading: false 
  } 
  componentDidMount() { 
    this.fetchData(); 
  } 
  componentDidUpdate(prevProps) { 
    if (this.props.url && this.props.url !== prevProps.url) {     
      this.fetchData(this.props.url); 
    } 
  } 
  async fetchData() { 
    try { 
      this.setState({ loading: true }); 
      const response = await fetch(this.props.url); 
      const json = await response.json(); 
      this.setState({ data: json }); 
      this.setState({ loading: false }); 
    catch (err) { 
      this.setState({ error: err }) 
    } 
  } 
  render() {
    const { error, data, loading } = this.state; 
    if(loading) return <div>Loading...</div>
    if(error) return this.props.error(error);
    if(data) return this.props.render(data); 
    else return null; 
  } 
}

要使用我们的组件,我们现在可以输入:

<Fetch 
  url={url-to-product} 
  render={(data) => <ProductDetail product={data.product} />} 
  error={(error) => <div>{error.message}</div>} 
/>

A/B 测试

让我们继续讨论下一个案例。我们迟早会因为两个主要原因想要使用此组件有条件地显示代码:

  • 它还没有准备好,我们希望经常部署,我们可能只想向我们的产品负责人展示一个新功能,这样我们就可以收集反馈,所以如果我们能够用一个标志来控制这些组件内容的显示,那就太好了
  • A/B 测试,假设我们不知道在我们的电子商务应用程序中我们想要进入哪个新的 Checkout 页面,那么如果我们可以将一半的用户发送到版本 1,而将另一半发送到版本 2,那就太好了。在这种情况下,您可能有两个不同的页面,但如果差异很小,例如切换几个部分,那么这可能是一个很好的选择。

好的,让我们看看如何使用这个组件:

<FeatureFlag 
  flag={showAlternateSection} 
  render={()=> <div>Alternate design</div>} 
  else={()=> <div>Normal design</div>} 
/>

上面我们有一个组件FeatureFlag和以下属性,让我们分解一下如何使用它们:

  • flag,这将是功能标志的名称,很可能是一个字符串
  • render,这将是我们在启用功能标志后调用的方法
  • 否则,如果功能标志被禁用或不存在,我们将调用该方法

构建我们的组件

好的,我们知道我们打算如何使用我们的组件,让我们尝试构建它:

class FeatureFlag extends React.Component { 
  state = { 
    enabled: void 0 
  } 

  componentDidMount() { 
    const enabled = localStorage.getItem(this.props.flag) === 'true'; 
    this.setState({ enabled }); 
  } 
  render() { 
    if(enabled) return this.props.render(); 
    else if(enabled === false) return this.props.else(); 
    else return null; 
  } 
}

好的,那么这里介绍三种状态:

  • true,当我们知道标志为真时
  • false,当我们知道标志为假时
  • void 0/undefined,当标志值尚未解析时

为什么我们需要三个状态?因为我们想确保它准确地渲染它应该渲染的内容,并且不会显示不该显示的内容,哪怕只有一毫秒。

好吧,这听起来有点疯狂,localStorage.getItem()通常反应很快。

是的,好的,我可能有点疯狂,但是如果标志值不在,localStorage但它驻留在我们需要调用的服务上,那么可能需要一些时间才能取回值......

想象一下我们的componentDidMount()样子:

async componentDidMount() { 
  const enabled = await flagService.get(this.props.flag);
  this.setState({ enabled }); 
}

如果您想将标志放在服务中而不是localStorage

概括

渲染 props 模式就是渲染一个函数,该函数本身渲染 JSX,正如你所见,你可以基于这个设计模式创建非常强大和有用的组件。

希望您发现这篇文章有用,如果觉得有用,请给我一些掌声。

哦,如果您在评论中给我留下一些使用渲染道具模式构建的组件的链接,我会很高兴。

保持安全,记住这不是你的错,而是编译器的错;)

进一步阅读

值得称赞的地方就称赞吧。要不是https://twitter.com/l0uy ,我也不会写这篇文章,所以关注一下他吧 :)

文章来源:https://dev.to/itnext/the-amazing-render-props-pattern-for-react-js-lifecycle-begone-433m
PREV
使用 firebase 验证用户身份并做出反应。
NEXT
逆向工程:如何用 JavaScript 构建测试库