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

20,161 阅读14分钟

前言

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


原文地址8 React conditional rendering methods

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

译者注:条件、循环表达式一般是模板引擎默认提供的最基本语法

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

但,条件表达式呢?

那就是另一个故事了。

你有很多选择

在React中有好几种方法可以实现条件表达式。并且,不同的方法适用于不同的场景,取决于你需要处理什么样的问题。

本文包含了最常见的几种条件渲染方法:

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

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

你可以在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中的高级概念(例如高阶组件)的接受程度

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