【譯】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中的高級概念(例如高階組件)的接受程度

固然,有些事是始終重要的,那就是保持簡單和可讀性。

相關文章
相關標籤/搜索