[譯] React 實現條件渲染的多種方式和性能考量

JSX 是對 JavaScript 強大的擴展,容許咱們來定義 UI 組件。可是它不直接支持循環和條件表達式(儘管添加 條件表達式已經被討論過了)。html

若是你想要遍歷一個列表來渲染多個組件或者實現一些條件邏輯,你不得不使用純 Javascript,你也並無不少的選擇來處理循環。更多的時候,map 將會知足你的須要。前端

可是條件表達式呢?react

那就是另一回事了。android

有幾種方案可供你選擇

在 React 中有多種使用條件語句的方式。而且,和編程中的大多數事情同樣,依賴於你所要解決的實際問題,有些方式是更適合的。ios

本教程介紹了最流行的條件渲染方法:git

  • If/Else
  • 避免渲染空元素
  • 元素變量
  • 三元運算符
  • 與運算 (&&)
  • 當即調用函數(IIFE)
  • 子組件
  • 高階組件(HOCs)

做爲全部這些方法如何工做的示例,接下來將實現具備查看/編輯功能的組件:程序員

你能夠在 JSFiddle 中嘗試和拷貝(fork)全部例子。github

讓咱們從使用 if/else 這種最原始的實現開始並在這裏構建它。web

If/else

讓咱們使用以下狀態來構建一個組件:算法

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
  }
}
複製代碼

你將使用一個屬性來保存文本,而且使用另一個屬性存儲正在被編輯的文本。第三個屬性將用來表示你是在 edit 仍是 view 模式下。

接下來,添加一些方法來處理輸入文本、保存和輸入事件:

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

  handleEdit() {
    this.setState({mode: 'edit'});
  }
}
複製代碼

如今,對於渲染方法,除了保存的文本以外,還要檢查模式狀態屬性,以顯示編輯按鈕或文本輸入框和保存按鈕:

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

  handleEdit() {
    this.setState({mode: 'edit'});
  }
}
複製代碼

下面是完整的代碼,能夠在 fiddle 中嘗試執行它:

Babel + JSX:

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

  handleEdit() {
    this.setState({mode: 'edit'});
  }
  
  render () {
    if(this.state.mode === 'view') {
      return (
        <div>
          <p>Text: {this.state.text}</p>
          <button onClick={this.handleEdit}>
            Edit
          </button>
        </div>
      );
    } else {
      return (
        <div>
          <p>Text: {this.state.text}</p>
            <input
              onChange={this.handleChange}
              value={this.state.inputText}
            />
          <button onClick={this.handleSave}>
            Save
          </button>
        </div>
      );
    }
  }
}

ReactDOM.render(
    <App />,
  document.getElementById('root')
);
複製代碼

if/else 是最簡單的方式來解決這個問題,可是我肯定你知道這並非一種好的實現方式。

它適用於簡單的用例,每一個程序員都知道它是如何工做的。可是有不少重複,render 方法看起來並不簡潔。

因此讓咱們經過將全部條件邏輯提取到兩個渲染方法來簡化它,一個來渲染文本框,另外一個來渲染按鈕:

class App extends React.Component {
  // …
  
  renderInputField() {
    if(this.state.mode === 'view') {
      return <div></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>
    );
  }
}
複製代碼

下面是完整的代碼,能夠在 fiddle 中嘗試執行它:

Babel + JSX:

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

  handleEdit() {
    this.setState({mode: 'edit'});
  }
  
  renderInputField() {
    if(this.state.mode === 'view') {
      return <div></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>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製代碼

須要注意的是當組件在預覽模式下時,方法 renderInputField 返回了一個空的 div 元素。

然而這並非必要的。

避免渲染空元素

若是你想要隱藏一個組件,你可讓它的渲染方法返回 null,由於不必渲染一個空的(和不一樣的)元素來佔位。

須要注意的重要一點是當返回 null 時,即便組件並不會被看見,可是生命週期方法仍然被觸發了。

舉個例子,下面的代碼實現了兩個組件之間的計數器:

Babel + JSX:

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 組件只有在父組件傳遞偶數時渲染父組件傳遞的值,不然,將返回 null。而後,當觀察控制檯輸出時,將會發現無論 render 返回什麼, componentDidUpdate 老是會被調用。

回頭來看咱們的例子,像這樣來改變 renderInputField 方法:

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

下面是完整的代碼:

Babel + JSX:

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

  handleEdit() {
    this.setState({mode: 'edit'});
  }
  
  renderInputField() {
    if(this.state.mode === 'view') {
      return null;
    } 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>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製代碼

返回 null 來替代一個空元素的優點在於這將會對組建的性能有一些改善,由於 React 沒必要要解綁組件來替換它。

例如,當執行返回空 div 元素的代碼時,打開檢閱頁面元素,將會看到在跟元素下的 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;
  }
複製代碼

這樣作是等同於那些返回 null 的方法的。

如下是優化後的完整代碼:

Babel + JSX:

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

  handleEdit() {
    this.setState({mode: 'edit'});
  }
  
  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 () {
    return (
      <div>
        <p>Text: {this.state.text}</p>
        {this.renderInputField()}
        {this.renderButton()}
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製代碼

使用這種方式使主 render 方法更有可讀性,可是可能並無必要使用 if/else 判斷(或者像 switch 這樣的語句)和輔助的渲染方法。

讓咱們嘗試一種更簡單的方法。

三元運算符

咱們可使用 三元運算符 來代替 if/else 語句:

condition ? expr_if_true : expr_if_false
複製代碼

該運算符用大括號包裹,表達式能夠包含JSX,可選擇將其包含在圓括號中以提升可讀性。

它能夠應用於組件的不一樣部分。讓咱們將它應用到示例中,以便您能夠看到這個實例。

我將在 render 方法中刪除 renderInputFieldrenderButton,並添加一個變量用來表示組件是在 view 仍是 edit 模式:

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

  return (
      <div>
      </div>
  );
}
複製代碼

如今,你可使用三元運算符,當組件被設置爲 view 模式時返回 null,不然返回輸入框:

// ...

  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>
  );
複製代碼

正如前面所說,三元運算符能夠應用在組件的不一樣位置。

能夠在 fiddle 中運行查看效果:

jsfiddle.net/eh3rrera/y6…

與運算符

在某種特殊狀況下,三元運算符是能夠簡化的。

當你想要一種條件下渲染元素,另外一種條件下不渲染元素時,你可使用 && 運算符。

不一樣於 & 運算符,當左側的表達式能夠決定最終結果時,&& 是不會再執行右側表達式的判斷的。

例如,若是第一個表達式被斷定爲 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>
)
複製代碼

下面是可在 fiddle 中執行的完整代碼:

Banel + JSX:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  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>
);
複製代碼

這可能會很快變得混亂。

出於這個緣由,有時您可能想要使用其餘技術,例如當即執行函數。

當即執行函數表達式 (IIFE)

顧名思義,當即執行函數就是在定義以後被當即調用的函數,他們不須要被顯式地調用。

一般狀況下,你通常會這樣定義並執行(定義後執行)一個函數:

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>
    );
  })()
} 
複製代碼

下面是能夠在 fiddle 中執行的完整代碼:

Babel + JSX:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  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>
          )
        }
        
        {
          (() => {
            const handler = view 
                ? this.handleEdit 
                : this.handleSave;
            const label = view ? 'Edit' : 'Save';
          
            return (
              <button onClick={handler}>
                {label}
              </button>
            );
          })()
        }  
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製代碼
<div id="root"></div>
複製代碼

子組件

不少時候,當即執行函數看起來多是一種不那麼優雅的解決方案。

畢竟,咱們在使用 React,React 推薦使用的方案是將你的應用邏輯分解爲儘量多的組件,而且推薦使用函數式編程而非命令式編程。

因此修改條件渲染邏輯爲一個子組件,這個子組件會依據父組件傳遞的 props 來決定在不一樣狀況下的渲染,這將會是一個更好的方案。

但在這裏,我將作一些有點不一樣的事情,向您展現如何從一個命令式的解決方案轉向更多的聲明式和函數式解決方案。

我將從建立一個 SaveComponent 組件開始:

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

正如函數式編程的屬性,SaveComponent 的功能邏輯都來自於它接收的參數所指定的。一樣的方式定義另外一個組件 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>
    );
}
複製代碼

下面是能夠在 fiddle 中執行的完整代碼:

Babel + JSX:

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

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

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  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
            ? <EditComponent handleEdit={this.handleEdit}  />
            : (
              <SaveComponent 
               handleChange={this.handleChange}
               handleSave={this.handleSave}
               text={this.state.inputText}
             />
            )
        } 
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製代碼

If 組件

有像 jsx-control-statements 這樣的庫能夠擴展JSX來添加以下條件語句:

<If condition={ true }>

  <span>Hi!</span>

</If>
複製代碼

這些庫提供更高級的組件,可是若是咱們須要簡單的 if/else,咱們能夠參考 Michael J. Ryanissue 下的 評論

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>
    );
}
複製代碼

下面是能夠在 fiddle 中執行的完整代碼:

Babel + JSX:

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

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

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

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  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';
    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>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製代碼

高階組件

高階組件(HOC)是一個函數,它接收一個已經存在的組件而且基於這個組件返回一個新的帶有更多附加功能的組件:

const EnhancedComponent = higherOrderComponent(component);
複製代碼

應用於條件渲染時,一個組件被傳遞給一個高階組件,高階組件能夠依據一些條件返回一個不一樣於原組件的組件:

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

    return <Component { ...props } />;
  };
}
複製代碼

這裏有一篇 Robin Wieruch 寫的 關於高階組件的精彩好文,這篇文章深刻討論了高階組件在條件渲染中的應用。

在咱們這篇文章中,我將會借鑑一些 EitherComponent 的概念。

在函數式編程中,Either 這一類方法的實現一般是做爲一個包裝,來返回兩個不一樣的值。

因此讓咱們從定義一個接收兩個參數的函數開始,另外一個函數返回一個布爾值(判斷條件的結果),若是這個布爾值爲 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>
    );
}
複製代碼

下面是能夠在 fiddle 中執行的完整代碼:

Babel + JSX:

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

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

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

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

const withEditContionalRendering = withEither(isViewConditionFn, EditComponent);
const EditSaveWithConditionalRendering = withEditContionalRendering(SaveComponent);

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

  handleEdit() {
    this.setState({mode: 'edit'});
  }
  
  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>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製代碼

性能考量

條件渲染多是複雜的。就像前面我所展現的那樣,每種方式的性能也多是不一樣的。

然而,在大多數時候這種差異是不成問題的。但當它確實形成問題時,你將須要深刻理解 React 的虛擬 DOM 的工做原理,而且使用一些技巧來優化性能

這裏有一篇關於很好的文章,關於 優化React的條件渲染,我很是推薦你讀一下。

基本的思想是條件渲染致使改變組件的位置將會引發迴流,從而致使應用內組件的解綁/綁定。

基於這篇文章的例子,我寫了兩個例子:

第一個例子使用 if/else 來控制 SubHeader 組件的顯示/隱藏:

Babel + JSX:

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')
);
複製代碼

第二個例子使用與運算(&&)作一樣的事情:

Babel + JSX:

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')
);
複製代碼

打開元素檢查而且點擊幾回按鈕。

你將看到在每一種實現中 Content 是被如何處理的。

結論

就像編程中的不少事情同樣,在 React 中有不少種方式實現條件渲染。

我會說除了第一種方式(有多種返回的if/else),你能夠任選你喜歡的方式。

基於下面的原則,你能夠決定哪種方式在你的實際狀況中是最好的:

  • 你的編程風格
  • 條件邏輯的複雜程度
  • 使用 JavaScript、JSX和高級的 React 概念(好比高階組件)的溫馨度。

若是全部的事情都是至關的,那麼就追求簡明度和可讀性。


Plug: LogRocket, a DVR for web apps

LogRocket 是一款前端日誌工具,可以在你本身的瀏覽器上覆現問題。而不是去猜爲何發生錯誤或者向用戶要截圖和日誌,LogRocket 幫助你復現場景來快速理解發生了什麼錯誤。 它適用於任何應用程序,且和框架無關,而且具備從Redux,Vuex和@ngrx/store記錄其餘上下文的插件。

除了記錄Redux動做和狀態以外,LogRocket 還記錄控制檯日誌,JavaScript 錯誤,堆棧跟蹤,帶有頭信息+主體的網絡請求/響應,瀏覽器元數據和自定義日誌。它還能夠檢測 DOM 來記錄頁面上的 HTML 和 CSS,即便是最複雜的單頁面應用,也能還原出像素級的視頻。

免費試用。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索