本文是譯者第一次作完整的全篇翻譯,主要目的是學習一下這類文章的寫做風格,因此挑了一篇相對入門、由淺入深的文章,全篇採用直譯,即便有時候以爲做者挺囉嗦的,也依然翻譯了原文內容。html
原文地址:8 React conditional rendering methodsreact
相較於Javascript,JSX是一個很好的擴展,它容許咱們定義UI組件。可是,它不提供條件、循環表達式的原生支持(增長條件表達式在該issue中被討論過)。git
譯者注:條件、循環表達式通常是模板引擎默認提供的最基本語法程序員
假設你須要遍歷一個列表,去渲染多個組件或者實現一些條件判斷邏輯,都必須用到JS。不過大部分狀況下,可選的方法不多,Array.prototype.map
都能知足需求。github
但,條件表達式呢?web
那就是另外一個故事了。算法
在React中有好幾種方法能夠實現條件表達式。而且,不一樣的方法適用於不一樣的場景,取決於你須要處理什麼樣的問題。編程
本文包含了最多見的幾種條件渲染方法:瀏覽器
爲了說明這些方法都是如何使用的,本文實現了一個編輯/展現態互相切換的組件:bash
你能夠在JSFiddle運行、體驗全部示例代碼。
譯者注:JSFiddle在牆內打開實在太慢了,故本文不貼出完整示例地址,若有須要,可自行查看原文連接。若是有合適的替代產品,歡迎告知
首先,咱們建立一個基礎組件:
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。一般來講,不推薦這麼作。
若是想隱藏一個組件,你能夠經過讓該組件的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來提高可讀性。
三元運算符能夠用在組件的不一樣地方(?),讓咱們在例子中實際應用看看。
譯者注:標記?的這句話我我的不是很理解
我先移除renderInputField
和renderButton
方法,並在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>
);
}
複製代碼
有些庫,例如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 } />;
複製代碼
你能夠用到以前定義的SaveComponent
和EditComponent
來建立一個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') ); 複製代碼
另外一個使用短路運算符(&&
)實現:
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
組件的表如今兩種實現中式不一致的。
譯者注:例子1中的寫法,Content每次都會被從新渲染
就像編程中的其餘事情同樣,在React中實現條件渲染有不少種實現方式。
你能夠自由選擇任一方式,除了第一種(if/else而且包含了不少return)。
你能夠基於這些理由來找到最適合當前場景的方案:
固然,有些事是始終重要的,那就是保持簡單和可讀性。