React.js:将一个组件包装到另一个组件中


187

许多模板语言都有“ slots”或“ yield”语句,它们允许进行某种形式的控制反转,以将一个模板包装在另一个模板中。

Angular具有“ transclude”选项

Rails有收益声明。如果React.js拥有yield语句,它将看起来像这样:

var Wrapper = React.createClass({
  render: function() {
    return (
      <div className="wrapper">
        before
          <yield/>
        after
      </div>
    );
  }
});

var Main = React.createClass({
  render: function() {
    return (
      <Wrapper><h1>content</h1></Wrapper>
    );
  }
});

所需的输出:

<div class="wrapper">
  before
    <h1>content</h1>
  after
</div>

React,React.js没有<yield/>。如何定义包装器组件以实现相同的输出?


Answers:



159

使用 children

const Wrapper = ({children}) => (
  <div>
    <div>header</div>
    <div>{children}</div>
    <div>footer</div>
  </div>
);

const App = ({name}) => <div>Hello {name}</div>;

const WrappedApp = ({name}) => (
  <Wrapper>
    <App name={name}/>
  </Wrapper>
);

render(<WrappedApp name="toto"/>,node);

transclusion在Angular中也称为。

children是React中的一个特殊道具,它将包含组件标签中的内容(这里<App name={name}/>是内部Wrapper,因此它是children

请注意,您不一定需要使用children,这对于组件是唯一的,并且您可以根据需要使用常规道具,也可以将道具和子项混合使用:

const AppLayout = ({header,footer,children}) => (
  <div className="app">
    <div className="header">{header}</div>
    <div className="body">{children}</div>
    <div className="footer">{footer}</div>
  </div>
);

const appElement = (
  <AppLayout 
    header={<div>header</div>}
    footer={<div>footer</div>}
  >
    <div>body</div>
  </AppLayout>
);

render(appElement,node);

对于许多用例来说,这是简单且很好的方法,我建议在大多数消费者应用程序中使用。


渲染道具

可以将render函数传递给组件,通常将这种模式称为render prop,并且childrenprop通常用于提供该回调。

此模式并非真正用于布局。包装器组件通常用于保存和管理某些状态,并将其注入其渲染函数中。

反例:

const Counter = () => (
  <State initial={0}>
    {(val, set) => (
      <div onClick={() => set(val + 1)}>  
        clicked {val} times
      </div>
    )}
  </State>
); 

您甚至可以花哨甚至提供一个物体

<Promise promise={somePromise}>
  {{
    loading: () => <div>...</div>,
    success: (data) => <div>{data.something}</div>,
    error: (e) => <div>{e.message}</div>,
  }}
</Promise>

请注意,您不一定需要使用children,这取决于口味/ API。

<Promise 
  promise={somePromise}
  renderLoading={() => <div>...</div>}
  renderSuccess={(data) => <div>{data.something}</div>}
  renderError={(e) => <div>{e.message}</div>}
/>

到今天为止,许多库都在使用渲染道具(React上下文,React-motion,Apollo ...),因为人们倾向于比HOC更容易找到此API。react-powerplug是简单的render-prop组件的集合。react-adopt帮助您进行写作。


高阶组件(HOC)。

const wrapHOC = (WrappedComponent) => {
  class Wrapper extends React.PureComponent {
    render() {
      return (
        <div>
          <div>header</div>
          <div><WrappedComponent {...this.props}/></div>
          <div>footer</div>
        </div>
      );
    }  
  }
  return Wrapper;
}

const App = ({name}) => <div>Hello {name}</div>;

const WrappedApp = wrapHOC(App);

render(<WrappedApp name="toto"/>,node);

一个高阶组件/ HOC通常是一个函数,它的成分,并返回一个新的组件。

使用高阶组件可能比使用children或具有更高的性能render props,这是因为包装器可以缩短渲染步骤的能力shouldComponentUpdate

在这里我们正在使用PureComponent。重新渲染应用程序时,如果WrappedApp名称prop不会随时间变化,则包装程序可以说“我不需要渲染,因为prop(实际上,名称)与以前相同”。使用上述children基于基础的解决方案,即使包装器为PureComponent,也不是这样,因为每次父级呈现时都会重新创建children元素,这意味着即使包装的组件是纯净的,包装器也将始终会重新呈现。有一个babel插件可以帮助缓解这种情况并确保children元素随时间推移保持不变。


结论

高阶组件可以为您提供更好的性能。它并不那么复杂,但是起初看起来确实不友好。

阅读本文后,请勿将整个代码库迁移到HOC。请记住,出于性能原因,在应用程序的关键路径上,您可能希望使用HOC而不是运行时包装器,特别是如果多次使用同一包装器,则值得考虑将其设为HOC。

Redux最初使用运行时包装<Connect>,后来connect(options)(Comp)出于性能原因切换到HOC (默认情况下,包装是纯净的并使用shouldComponentUpdate)。这是我在此答案中要强调的内容的完美例证。

请注意,如果组件具有render-prop API,通常很容易在其之上创建HOC,因此,如果您是lib作者,则应首先编写render prop API,并最终提供HOC版本。这是Apollo对<Query>render-prop组件所做的,graphqlHOC也在使用它。

就我个人而言,我同时使用这两种方法,但是在有疑问时,我更喜欢HOC,因为:

  • compose(hoc1,hoc2)(Comp)与渲染道具相比,组合它们()更惯用
  • 它可以给我更好的表现
  • 我熟悉这种编程风格

我会毫不犹豫地使用/创建我喜欢的工具的HOC版本:

  • 反应的Context.Consumer补偿
  • 未声明的 Subscribe
  • 使用graphqlApollo的HOC代替Query渲染道具

在我看来,有时渲染道具会使代码更具可读性,有时则使代码更具可读性...我尝试根据我的约束使用最实用的解决方案。有时可读性比性能更重要,有时则不那么重要。明智地选择,不要束缚地遵循2018年将一切都转换为render-props的趋势。


1
这种方法还可以轻松地将props传递给do children组件(在本例中为Hello)。从React 0.14。*​​开始,将prop传递给子组件的唯一方法是使用React.createClone,这可能很昂贵。
Mukesh Soni 2015年

2
问题:答案提到“更好的性能”-我不了解:与其他解决方案相比更好?
菲利普

1
与运行时包装器相比,HOC可以具有更好的性能,因为它们可以使渲染更早地短路。
塞巴斯蒂安·洛伯

1
谢谢!就像您听了我一个月的话,但是您用更大的才华表达了他们
-MacKentoch

1
这是一个更好的答案:]谢谢!
cullanrocks

31

除了Sophie的回答,我还发现了在发送子组件类型中的一种用途,它可以执行以下操作:

var ListView = React.createClass({
    render: function() {
        var items = this.props.data.map(function(item) {
            return this.props.delegate({data:item});
        }.bind(this));
        return <ul>{items}</ul>;
    }
});

var ItemDelegate = React.createClass({
    render: function() {
        return <li>{this.props.data}</li>
    }
});

var Wrapper = React.createClass({    
    render: function() {
        return <ListView delegate={ItemDelegate} data={someListOfData} />
    }
});

2
我还没有看过任何文档delegate,您是怎么找到它的?
NVI 2014年

4
您可以在组件中添加所需的任何props并根据需要命名它们,即时消息使用第4行的this.props.delegate,但也可以为其命名。
2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.