【譯】React的8種條件渲染方法

Nekron發表於2019-03-04

前言

本文是譯者第一次做完整的全篇翻譯,主要目的是學習一下這類文章的寫作風格,所以挑了一篇相對入門、由淺入深的文章,全篇採用直譯,即使有時候覺得作者挺囉嗦的,也依然翻譯了原文內容。


原文地址8 React conditional rendering methods

相較於Javascript,JSX是一個很好的擴充套件,它允許我們定義UI元件。但是,它不提供條件、迴圈表示式的原生支援(增加條件表示式在該issue中被討論過)。

譯者注:條件、迴圈表示式一般是模板引擎預設提供的最基本語法

假設你需要遍歷一個列表,去渲染多個元件或者實現一些條件判斷邏輯,都必須用到JS。不過大部分情況下,可選的方法很少,Array.prototype.map都能滿足需求。

但,條件表示式呢?

那就是另一個故事了。

你有很多選擇

在React中有好幾種方法可以實現條件表示式。並且,不同的方法適用於不同的場景,取決於你需要處理什麼樣的問題。

本文包含了最常見的幾種條件渲染方法:

  • If/Else
  • 返回null阻止渲染
  • 變數
  • 三元運算子
  • 短路運算子(&&)
  • 自執行函式(IIFE)
  • 子元件
  • 高階元件(HOCs)

為了說明這些方法都是如何使用的,本文實現了一個編輯/展示態互相切換的元件:

【譯】React的8種條件渲染方法

你可以在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元素會始終更新:

【譯】React的8種條件渲染方法

相對的,如果是返回null,當Edit按鈕被點選時,這個div元素不會更新:

【譯】React的8種條件渲染方法

你可以在這裡繼續深入瞭解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中的高階概念(例如高階元件)的接受程度

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

相關文章