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

Angular有“transclude”选项。

Ruby/Rails有yield语句。如果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.js没有<yield/>。如何定义Wrapper组件来实现相同的输出?


Try:

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

更多信息请参见文档中的多重组件:子组件和子组件的类型。


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

使用儿童

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

这在Angular中也称为transclusion。

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

注意,你不一定需要使用子组件,这对于一个组件来说是唯一的,如果你愿意,你也可以使用普通的道具,或者混合使用props和子组件:

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

这对很多用例来说都很简单,我建议大多数消费者应用程序都这样做。


渲染道具

可以将渲染函数传递给组件,这种模式通常称为渲染道具,子道具通常用于提供回调。

这个模式实际上并不用于布局。包装器组件通常用于保存和管理一些状态,并将其注入到其呈现函数中。

反例:

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>

注意,你不一定需要使用子元素,这是一个口味/API的问题。

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

到今天为止,许多库都在使用渲染道具(React context, React-motion, Apollo…),因为人们倾向于发现这个API比HOC的更简单。React-powerplug是一个简单渲染道具组件的集合。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);

高阶组件(high - order Component / HOC)通常是接受一个组件并返回一个新组件的函数。

使用高阶组件比使用子组件或呈现道具的性能更好,因为包装器可以通过shouldComponentUpdate提前一步缩短呈现。

Here we are using PureComponent. When re-rendering the app, if the WrappedApp name prop does not change over time, the wrapper has the ability to say "I don't need to render because props (actually, the name) are the same as before". With the children based solution above, even if the wrapper is PureComponent, it is not the case because the children element is recreated everytime the parent renders, which means the wrapper will likely always re-render, even if the wrapped component is pure. There is a babel plugin that can help mitigate this and ensure a constant children element over time.


结论

高阶组件可以提供更好的性能。这并不复杂,但一开始看起来确实不友好。

阅读本文后不要将整个代码库迁移到HOC。只要记住,在应用程序的关键路径上,出于性能原因,你可能想要使用HOC而不是运行时包装器,特别是如果相同的包装器被使用了很多次,就值得考虑将其设置为HOC。

Redux首先使用运行时包装器<Connect>,后来出于性能原因切换到HOC连接(options)(Comp)(默认情况下,包装器是纯的并使用shouldComponentUpdate)。这是我想在这个答案中强调的完美例证。

注意,如果一个组件有一个渲染道具API,通常很容易在它上面创建一个HOC,所以如果你是一个库作者,你应该先写一个渲染道具API,并最终提供一个HOC版本。这就是Apollo使用<Query>渲染道具组件和graphql HOC使用它所做的。

就我个人而言,我两者都用,但当有疑问时,我更喜欢hoc,因为:

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

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

反应的上下文。消费者比较 未明确说明的的订阅 使用Apollo的graphql HOC代替查询渲染道具

在我看来,渲染道具有时会让代码更有可读性,有时则会降低……根据我所拥有的限制,我尝试使用最实用的解决方案。有时可读性比性能更重要,有时不是。明智地选择,不要拘泥于2018年将所有东西都转换为渲染道具的趋势。