【译】React的8种条件渲染方法

前言

本文是译者第一次作完整的全篇翻译,主要目的是学习一下这类文章的写做风格,因此挑了一篇相对入门、由浅入深的文章,全篇采用直译,即便有时候以为做者挺啰嗦的,也依然翻译了原文内容。html


原文地址8 React conditional rendering methodsreact

相较于Javascript,JSX是一个很好的扩展,它容许咱们定义UI组件。可是,它不提供条件、循环表达式的原生支持(增长条件表达式在该issue中被讨论过)。git

译者注:条件、循环表达式通常是模板引擎默认提供的最基本语法程序员

假设你须要遍历一个列表,去渲染多个组件或者实现一些条件判断逻辑,都必须用到JS。不过大部分状况下,可选的方法不多,Array.prototype.map都能知足需求。github

但,条件表达式呢?web

那就是另外一个故事了。算法

你有不少选择

在React中有好几种方法能够实现条件表达式。而且,不一样的方法适用于不一样的场景,取决于你须要处理什么样的问题。编程

本文包含了最多见的几种条件渲染方法:浏览器

  • If/Else
  • 返回null阻止渲染
  • 变量
  • 三元运算符
  • 短路运算符(&&)
  • 自执行函数(IIFE)
  • 子组件
  • 高阶组件(HOCs)

为了说明这些方法都是如何使用的,本文实现了一个编辑/展现态互相切换的组件:bash

你能够在JSFiddle运行、体验全部示例代码。

译者注:JSFiddle在墙内打开实在太慢了,故本文不贴出完整示例地址,若有须要,可自行查看原文连接。若是有合适的替代产品,欢迎告知

If/Else

首先,咱们建立一个基础组件:

class App extends React.Component {
  state = {
    text: '', 
    inputText: '', 
    mode: 'view',
  }
}
复制代码

text属性存储已存的文案,inputText属性存储输入的文案,mode属性来存储当前是编辑态仍是展现态。

接下来,咱们增长一些方法来处理input输入以及状态切换:

class App extends React.Component {
  state = {
    text: '', 
    inputText: '', 
    mode: 'view',
  }
  
  handleChange = (e) => {
    this.setState({ inputText: e.target.value });
  }
  
  handleSave = () => {
    this.setState({text: this.state.inputText, mode: 'view'});
  }

  handleEdit = () => {
    this.setState({mode: 'edit'});
  }
}
复制代码

如今到了render方法,咱们须要检测state中的mode属性来决定是渲染一个编辑按钮仍是一个文本输入框+一个保存按钮:

class App extends React.Component {
  // …
  render () {
    if(this.state.mode === 'view') {
      return (
        <div> <p>Text: {this.state.text}</p> <button onClick={this.handleEdit}> Edit </button> </div>
      );
    } else {
      // 译者注:若是if代码块里有return时,通常不须要写else代码块,不过为了贴合标题仍是保留了
      return (
        <div> <p>Text: {this.state.text}</p> <input onChange={this.handleChange} value={this.state.inputText} /> <button onClick={this.handleSave}> Save </button> </div> ); } } 复制代码

If/Else是最简便的实现条件渲染的方法,不过我确定,你不认为这是一个好的实现方式。

它的优点是,在简单场景下使用方便,而且每一个程序员都理解这种使用方式;它的劣势是,会存在一些重复代码,而且render方法会变得臃肿。

那咱们来简化一下,咱们把全部的条件判断逻辑放入两个render方法,一个用来渲染输入框,另外一个用来渲染按钮:

class App extends React.Component {
  // …
  
  renderInputField() {
    if (this.state.mode === 'view') {
      return <div />;
    } else {
      return (
          <p>
            <input
              onChange={this.handleChange}
              value={this.state.inputText}
            />
          </p>
      );
    }
  }
  
  renderButton() {
    if (this.state.mode === 'view') {
      return (
          <button onClick={this.handleEdit}>
            Edit
          </button>
      );
    } else {
      return (
          <button onClick={this.handleSave}>
            Save
          </button>
      );
    }
  }

  render() {
    return (
      <div>
        <p>Text: {this.state.text}</p>
        {this.renderInputField()}
        {this.renderButton()}
      </div>
    );
  }
}
复制代码

注意在示例中,renderInputField函数在视图模式下,返回的是一个空div。一般来讲,不推荐这么作。

返回null阻止渲染

若是想隐藏一个组件,你能够经过让该组件的render函数返回null,不必使用一个空div或者其余什么元素去作占位符。

须要注意的是,即便返回了null,该组件“不可见”,但它的生命周期依然会运行。

举个例子,下面的例子用两个组件实现了一个计数器:

class Number extends React.Component {
  constructor(props) {
    super(props);
  }
  
  componentDidUpdate() {
    console.log('componentDidUpdate');
  }
  
  render() {
    if (this.props.number % 2 == 0) {
        return (
            <div> <h1>{this.props.number}</h1> </div>
        );
    } else {
      return null;
    }
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 }
  }
  
  onClick(e) {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  }

  render() {
    return (
      <div> <Number number={this.state.count} /> <button onClick={this.onClick.bind(this)}>Count</button> </div> ) } } ReactDOM.render( <App />, document.getElementById('root') ); 复制代码

Number组件只有在偶数时才会展现。由于奇数时,render函数返回了null。可是,当你查看console时会发现,componentDidUpdate函数每次都会执行,不管render函数返回什么。

回到本文的例子,咱们对renderInputField函数稍做修改:

renderInputField() {
    if (this.state.mode === 'view') {
      return null;
    } else {
      return (
          <p> <input onChange={this.handleChange} value={this.state.inputText} /> </p> ); } } 复制代码

此外,返回null而不是空div的另外一个好处是,这能够略微提高整个React应用的性能,由于React不须要在更新的时候unmount这个空div。

举个例子,若是是返回空div,在控制台中,你能够发现,root节点下的div元素会始终更新:

相对的,若是是返回null,当Edit按钮被点击时,这个div元素不会更新:

你能够在这里继续深刻了解React是如何更新DOM元素,以及调和算法是如何工做的。

在这个简单的例子中,也许这点性能差距是微不足道的,但若是是一个大型组件,性能差距就不容忽视。

我会在下文继续讨论条件渲染的性能影响。不过如今,让咱们先继续聚焦在这个例子上。

变量

有时候,我不喜欢在一个方法中包含多个return。因此,我会使用一个变量去指向这个JSX元素,而且只有当条件为true的时候才去初始化。

renderInputField() {
    let input;
    
    if (this.state.mode !== 'view') {
      input = 
        <p> <input onChange={this.handleChange} value={this.state.inputText} /> </p>; } return input; } renderButton() { let button; if (this.state.mode === 'view') { button = <button onClick={this.handleEdit}> Edit </button>; } else { button = <button onClick={this.handleSave}> Save </button>; } return button; } 复制代码

这些方法的返回结果和上一节的两个方法返回一致。

如今,render函数会变得更易读,不过在本例中,其实不必使用if/else(或者switch)代码块,也不必使用多个render方法。

咱们能够写得更简洁一些。

三元运算符

咱们可使用三元运算符替代if/else代码块:

condition ? expr_if_true : expr_if_false
复制代码

整个运算符能够放在jsx的{}中,每个表达式能够用()来包裹JSX来提高可读性。

三元运算符能够用在组件的不一样地方(?),让咱们在例子中实际应用看看。

译者注:标记?的这句话我我的不是很理解

我先移除renderInputFieldrenderButton方法,并在render中增长一个变量来表示组件是处于view模式仍是edit模式:

render () {
  const view = this.state.mode === 'view';

  return (
      <div> </div>
  );
}
复制代码

接下来,添加三元运算符——当处于view模式时,返回null;处于edit模式时,返回输入框:

// ...

  return (
      <div> <p>Text: {this.state.text}</p> { view ? null : ( <p> <input onChange={this.handleChange} value={this.state.inputText} /> </p> ) } </div> ); 复制代码

经过三元运算符,你能够经过改变组件内的标签或者回调函数来渲染一个保存/编辑按钮:

// ...

  return (
      <div> <p>Text: {this.state.text}</p> { ... } <button onClick={ view ? this.handleEdit : this.handleSave } > {view ? 'Edit' : 'Save'} </button> </div>
  );
复制代码

短路运算符

三元运算符在某些场景下能够更加简化。例如,当你要么渲染一个组件,要么不作渲染,你可使用&&运算符。

不像&运算符,若是&&执行左侧的表达式就能够确认结果的话,右侧表达式将不会执行。

举个例子,若是左侧表达式结果为false(false && ...),那么下一个表达式就不须要执行,由于结果永远都是false。

在React中,你能够这样运用:

return (
    <div> { showHeader && <Header /> } </div>
);
复制代码

若是showHeader结果为true,那么<Header />组件就会被返回;若是showHeader结果为false,那么<Header />组件会被忽略,返回的会是一个空div

上文的代码中:

{
  view
  ? null
  : (
    <p> <input onChange={this.handleChange} value={this.state.inputText} /> </p> ) } 复制代码

能够被改成:

!view && (
  <p> <input onChange={this.handleChange} value={this.state.inputText} /> </p> ) 复制代码

如今,完整的例子以下:

class App extends React.Component {
  state = {
    text: '',
    inputText: '',
    mode: 'view',
  }
  
  handleChange = (e) => {
    this.setState({ inputText: e.target.value });
  }
  
  handleSave = () => {
    this.setState({ text: this.state.inputText, mode: 'view' });
  }

  handleEdit = () => {
    this.setState({mode: 'edit'});
  }
  
  render () {
    const view = this.state.mode === 'view';
    
    return (
      <div> <p>Text: {this.state.text}</p> { !view && ( <p> <input onChange={this.handleChange} value={this.state.inputText} /> </p> ) } <button onClick={ view ? this.handleEdit : this.handleSave } > {view ? 'Edit' : 'Save'} </button> </div> ); } } ReactDOM.render( <App />, document.getElementById('root') ); 复制代码

这样看上去是否是好了不少?

然而,三元运算符有时候会让人困扰,好比以下的复杂代码:

return (
  <div> { condition1 ? <Component1 /> : ( condition2 ? <Component2 /> : ( condition3 ? <Component3 /> : <Component 4 /> ) ) } </div> ); 复制代码

很快,这些代码会变为一团乱麻,所以,有时候你须要一些其余技巧,好比:自执行函数。

自执行函数

顾名思义,自执行函数就是在定义之后会被马上执行,没有必要显式地调用他们。

一般来讲,函数是这么被定义并执行的:

function myFunction() {
// ...
}
myFunction();
复制代码

若是你指望一个函数在被定之后马上执行,你须要使用括号将整个定义包起来(将函数做为一个表达式),而后传入须要使用的参数。

示例以下:

( function myFunction(/* arguments */) {
    // ...
}(/* arguments */) );
复制代码

或:

( function myFunction(/* arguments */) {
    // ...
} ) (/* arguments */);
复制代码

若是这个函数不会在其余地方被调用,你能够省略名字:

( function (/* arguments */) {
    // ...
} ) (/* arguments */);
复制代码

或使用箭头函数:

( (/* arguments */) => {
    // ...
} ) (/* arguments */);
复制代码

在React中,你能够用一个大括号包裹一整个自执行函数,把全部逻辑都放在里面(if/else、switch、三元运算符等等),而后返回你须要渲染的东西。

举个例子,若是使用自执行函数去渲染一个编辑/保存按钮,代码会是这样的:

{
  (() => {
    const handler = view 
                ? this.handleEdit 
                : this.handleSave;
    const label = view ? 'Edit' : 'Save';
          
    return (
      <button onClick={handler}> {label} </button>
    );
  })()
} 
复制代码

子组件

有时候,自执行函数看上去像是黑科技。

使用React的最佳实践是,尽量地将逻辑拆分在各个组件内,使用函数式编程,而不是命令式编程。

因此,将条件渲染的逻辑放入一个子组件,子组件经过props来渲染不一样的内容会是一个不错的方案。

但在这里,我不这么作,在下文中我会向你展现一种更声明式、更函数式的写法。

首先,我建立一个SaveComponent

const SaveComponent = (props) => {
  return (
    <div> <p> <input onChange={props.handleChange} value={props.text} /> </p> <button onClick={props.handleSave}> Save </button> </div> ); }; 复制代码

经过props它接受足够的数据来供它展现。一样的,我再写一个EditComponent

const EditComponent = (props) => {
  return (
    <button onClick={props.handleEdit}> Edit </button>
  );
};
复制代码

render方法如今看起来会是这样:

render () {
    const view = this.state.mode === 'view';
    
    return (
      <div>
        <p>Text: {this.state.text}</p>
        
        {
          view
            ? <EditComponent handleEdit={this.handleEdit}  />
            : (
              <SaveComponent 
               handleChange={this.handleChange}
               handleSave={this.handleSave}
               text={this.state.inputText}
             />
            )
        } 
      </div>
    );
}
复制代码

If组件

有些库,例如JSX Control Statements,它们经过扩展JSX去支持条件状态:

<If condition={ true }>
  <span>Hi!</span>
</If>
复制代码

这些库提供了更多高级的组件,不过,若是咱们只须要一些简单的if/else,咱们能够写一个组件,相似Michael J. Ryan在这个issue的回复中提到的:

const If = (props) => {
  const condition = props.condition || false;
  const positive = props.then || null;
  const negative = props.else || null;
  
  return condition ? positive : negative;
};

// …

render () {
    const view = this.state.mode === 'view';
    const editComponent = <EditComponent handleEdit={this.handleEdit}  />;
    const saveComponent = <SaveComponent 
               handleChange={this.handleChange}
               handleSave={this.handleSave}
               text={this.state.inputText}
             />;
    
    return (
      <div>
        <p>Text: {this.state.text}</p>
        <If
          condition={ view }
          then={ editComponent }
          else={ saveComponent }
        />
      </div>
    );
}
复制代码

高阶组件

高阶组件(HOC)指的是一个函数,它接受一个已存在的组件,而后返回一个新的组件而且新增了一些方法:

const EnhancedComponent = higherOrderComponent(component);
复制代码

应用在条件渲染中,一个高阶组件能够经过一些条件,返回不一样的组件:

function higherOrderComponent(Component) {
  return function EnhancedComponent(props) {
    if (condition) {
      return <AnotherComponent { ...props } />;
    }

    return <Component { ...props } />;
  };
}
复制代码

这篇Robin Wieruch写的精彩文章中,他对使用高阶组件来完成条件渲染有更深刻的研究。

经过这篇文章,我准备借鉴EitherComponent的概念。

在函数式编程中,Ether常常被用来作一层包装以返回两个不一样的值。

让咱们先定义一个函数,它接受两个函数类型的参数,第一个函数会返回一个布尔值(条件表达式执行的结果),另外一个是当结果为true时返回的组件。

function withEither(conditionalRenderingFn, EitherComponent) {

}
复制代码

这种高阶组件的名字通常以with开头。

这个函数会返回一个函数,它接受原始组件为参数,并返回一个新组件:

function withEither(conditionalRenderingFn, EitherComponent) {
    return function buildNewComponent(Component) {

    }
}
复制代码

再内层的函数返回的组件将是你在应用中使用的,因此它须要接受一些属性来运行:

function withEither(conditionalRenderingFn, EitherComponent) {
    return function buildNewComponent(Component) {
        return function FinalComponent(props) {

        }
    }
}
复制代码

由于内层函数能够拿到外层函数的参数,因此,基于conditionalRenderingFn的返回值,你能够返回EitherComponent或者是原始的Component

function withEither(conditionalRenderingFn, EitherComponent) {
    return function buildNewComponent(Component) {
        return function FinalComponent(props) {
            return conditionalRenderingFn(props)
                ? <EitherComponent { ...props } />
                 : <Component { ...props } />;
        }
    }
}
复制代码

或者,使用箭头函数:

const withEither = (conditionalRenderingFn, EitherComponent) => (Component) => (props) =>
  conditionalRenderingFn(props)
    ? <EitherComponent { ...props } />
    : <Component { ...props } />;
复制代码

你能够用到以前定义的SaveComponentEditComponent来建立一个withEditConditionalRendering高阶组件,最终,建立一个EditSaveWithConditionalRendering组件:

const isViewConditionFn = (props) => props.mode === 'view';

const withEditContionalRendering = withEither(isViewConditionFn, EditComponent);
const EditSaveWithConditionalRendering = withEditContionalRendering(SaveComponent);
复制代码

译者注:苍了个天,杀鸡用牛刀

最终,在render中,你传入全部须要用到的属性:

render () {    
    return (
      <div> <p>Text: {this.state.text}</p> <EditSaveWithConditionalRendering mode={this.state.mode} handleEdit={this.handleEdit} handleChange={this.handleChange} handleSave={this.handleSave} text={this.state.inputText} /> </div> ); } 复制代码

性能的注意事项

条件渲染有时很微妙,上文中提到了不少方法,它的性能是不同的。

然而,大部分场景下,这些差别不算什么。可是当你须要作的时候,你须要对React的虚拟DOM是如何运转有很好的理解,而且掌握一些优化技巧

这里有篇关于优化条件渲染的文章,我推荐阅读。

核心点是,若是条件渲染的组件会引发位置的变动,那它会引发重排,从而致使app中的组件装载/卸载。

译者注:这里的重排指的不是浏览器渲染的重排,算是虚拟DOM的概念

基于文中的例子,我作了以下两个例子。

第一个使用if/else来展现/隐藏SubHeader组件:

const Header = (props) => {
  return <h1>Header</h1>;
}

const Subheader = (props) => {
  return <h2>Subheader</h2>;
}

const Content = (props) => {
  return <p>Content</p>;
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};
    
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }
  
  render() {
    if(this.state.isToggleOn) {
      return (
        <div> <Header /> <Subheader /> <Content /> <button onClick={this.handleClick}> { this.state.isToggleOn ? 'ON' : 'OFF' } </button> </div>
      );
    } else {
      return (
        <div> <Header /> <Content /> <button onClick={this.handleClick}> { this.state.isToggleOn ? 'ON' : 'OFF' } </button> </div>
      );
    }
  }
}

ReactDOM.render(
    <App />, document.getElementById('root') ); 复制代码

fiddle地址

另外一个使用短路运算符(&&)实现:

const Header = (props) => {
  return <h1>Header</h1>;
}

const Subheader = (props) => {
  return <h2>Subheader</h2>;
}

const Content = (props) => {
  return <p>Content</p>;
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};
    
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }
  
  render() {
    return (
      <div> <Header /> { this.state.isToggleOn && <Subheader /> } <Content /> <button onClick={this.handleClick}> { this.state.isToggleOn ? 'ON' : 'OFF' } </button> </div>
    );
  }
}

ReactDOM.render(
    <App />, document.getElementById('root') ); 复制代码

fiddle地址

打开控制台,并屡次点击按钮,你会发现Content组件的表如今两种实现中式不一致的。

译者注:例子1中的写法,Content每次都会被从新渲染

结论

就像编程中的其余事情同样,在React中实现条件渲染有不少种实现方式。

你能够自由选择任一方式,除了第一种(if/else而且包含了不少return)。

你能够基于这些理由来找到最适合当前场景的方案:

  • 你的编程风格
  • 条件逻辑的复杂度
  • 你对于Javascript、JSX和React中的高级概念(例如高阶组件)的接受程度

固然,有些事是始终重要的,那就是保持简单和可读性。

相关文章
相关标签/搜索