這一週連續發表了兩篇關於 React 的文章:
其中涉及到 React 元件複用、輪子設計相關話題,並配合相關場景例項進行了分析。這些內容都算是 React 設計模式,一提到 Design Patterns,讀者大可不必恐懼,事實上這都是 React 開發應用靈活性的體現。今天這篇文章,我們繼續通過一個場景,循序漸進,通過一步步優化設計來進行加深理解。
場景介紹
螢幕左側大面積展現區塊內容,點選 continue 按鈕,切換為下條內容資訊;右側是一個導航條,指示當前區塊展示資訊條目。
如果看 Gif 圖不過癮,可以到 CodeSandbox 進行線上瞭解。
具體程式碼結構為:
class App extends Component {
render() {
return (
<Stepper stage={1}/>
);
}
}
複製程式碼
Stepper 元件 'stage' prop 表示預設開始第幾個區塊,同時具用同名 'stage' 狀態。stage 在這裡表示左側一個個內容區塊。 handleClick 方法對 this.stata.stage 進行切換。
class Stepper extends Component {
state = {
stage: this.props.stage
}
static defaultProps = {
stage: 1
}
handleClick = () => {
this.setState({ stage: this.state.stage + 1 })
}
render() {
const { stage } = this.state;
return (
<div style={styles.container}>
<Progress stage={stage}/>
<Steps handleClick={this.handleClick} stage={stage}/>
</div>
);
}
}
複製程式碼
我們看到,Stepper 元件包含 Progress 元件(左側導航)以及 Steps 元件。 這樣的程式碼執行良好,但是在複用性和靈活性上有一些問題。比如:
- 如果我們需要切換 Progress 和 Steps 元件(左右)展示順序怎麼辦?
- 如果我們的 Stepper 需要承載更多的 stages 怎麼辦?
- 如果我們需要更改某個 stage 內容怎麼辦?
- 如果我們想要切換 stages 順序該怎麼辦?
現有程式碼基礎上,這些問題都可以解決。但是需要重新更改元件編寫內容。如果某天又新增或者調整了需求,元件內容同樣又需要改寫。
接下來,我們用另一種方式實現需求,使得程式碼更加靈活,複用性更強。
重新設計
仔細觀察 Stepper 元件:它包含了當前區塊 stage,以及一個更改 stage 的方法,渲染了兩個子元件。
我們使用 Function as Child Component 手段,將 Stepper 元件重構。(如果對 Function as Child Component 不熟悉,請參考我之前文章 元件複用那些事兒 - React 實現按需載入輪子)
如下圖:
Progress 和 Steps 元件不再直接出現在 Stepper 元件的 render 方法中。我們使用 this.props.children 對 Stepper 元件的所有子元件進行渲染。這樣 Stepper 元件渲染的內容更加靈活。
但是僅僅這樣的修改是不可能完成需求的,當使用者點選 continue 按鈕,stage 並不會進行切換。因為 Progress 和 Steps 元件無法再通過 props 感知 stage 和 handleClick 方法。
為了解決這個問題,我們可以手動遍歷 Stepper 元件的子節點,並對相應 props 一一注入。如下程式碼:
const children = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {stage, handleClick: this.handleClick})
})
複製程式碼
藉助 React.Children.map 進行子節點遍歷,並通過 React.cloneElement 方法對子元件進行拷貝,這個方法通過第二個引數,具有新增額外 props 的能力。Stepper 元件的 render 方法只需要具體應用:
const { stage } = this.state;
const children = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {stage, handleClick: this.handleClick})
})
return (
<div style={styles.container}>
{children}
</div>
);
複製程式碼
這樣一來,應用又一次正確運轉!
class App extends Component {
render() {
return (
<div>
<Stepper stage={1}>
<Progress />
<Steps />
</Stepper>
</div>
);
}
}
複製程式碼
同樣的手段,我們也可以應用到 Progress 元件當中。這裡不再一一展開。
使用 Static Properties
值得一提的是,我們可以使用 Static Properties 增強程式碼的可讀性。Static Properties 允許我們在 class 當中直接對方法進行呼叫。首先,我們在 Stepper 元件中建立兩個 static 方法,並賦值給 Progress 元件和 Steps 元件:
static Progress = Progress;
static Steps = Steps
複製程式碼
現在,在 App.js 中我們可以直接:
import React, { Component } from 'react';
import Stepper from "./Stepper"
class App extends Component {
render() {
return (
<Stepper stage={1}>
<Stepper.Progress />
<Stepper.Steps />
</Stepper>
);
}
}
export default App;
複製程式碼
這樣的好處體現在不用一次次地 import 進來 Progress 元件和 Steps 元件,它們都將作為 Stepper 的靜態屬性出現。我個人並不是很喜歡這種做法。
使用 React Transition Group
我們使用 React Transition Group 對 Steps 元件內容新增過渡動畫。只有當 props.num 與 this.props.stage 相等時,區塊內容設定為可見:
class Steps extends Component {
render() {
const {stage,handleClick} = this.props
const children = React.Children.map(this.props.children, child => {
console.log(child.props)
return (
stage === child.props.num &&
<Transition appear={true} timeout={300} onEntering={entering} onExiting={exiting}>
{child}
</Transition>
)
})
return (
<div style={styles.stagesContainer}>
<div style={styles.stages}>
<TransitionGroup>
{children}
</TransitionGroup>
</div>
<div style={styles.stageButton}>
<Button disabled={stage === 4} click={handleClick}>Continue</Button>
</div>
</div>
);
}
}
複製程式碼
我們也可以給 Steps 元件新增任意個內容:
import Stepper from "./Stepper"
class App extends Component {
render() {
return (
<Stepper stage={1}>
<Stepper.Progress>
<Stepper.Stage num={1} />
<Stepper.Stage num={2} />
<Stepper.Stage num={3} />
</Stepper.Progress>
<Stepper.Steps>
<Stepper.Step num={1} text={"Stage 1"}/>
<Stepper.Step num={2} text={"Stage 2"}/>
<Stepper.Step num={3} text={"Stage 3"}/>
<Stepper.Step num={4} text={"Stage 4"}/>
</Stepper.Steps>
</Stepper>
);
}
}
複製程式碼
重新設計之後,整個應用變得更加靈活,複用性更強。我們可以指定任意個 stages,每一個 stage 文字內容也可以自定義設定,同樣 stages 排列順序等都可以隨意搭配。
重構程式碼以及效果可以訪問這裡檢視。
思考及待續
如果你覺得上述程式碼完美無懈可擊,那顯然想簡單了。需求是變化多端的,如果我們想在 Steps 區塊上,加一個大標題呢?
class App extends Component {
render() {
return (
<Stepper stage={1}>
<Stepper.Progress>
<Stepper.Stage num={1} />
<Stepper.Stage num={2} />
<Stepper.Stage num={3} />
</Stepper.Progress>
<div>
<div>Title</div>
<Stepper.Steps>
<Stepper.Step num={1} text={"Stage 1"}/>
<Stepper.Step num={2} text={"Stage 2"}/>
<Stepper.Step num={3} text={"Stage 3"}/>
<Stepper.Step num={4} text={"Complete!"}/>
</Stepper.Steps>
</div>
</Stepper>
);
}
}
複製程式碼
如圖,
這樣一來,Stepper.Steps 元件再也不是 Stepper 元件的直接唯一子節點了,那預期之中的 props 自然又一次無法取得!
問題也不僅僅於此。筆者本人不是很喜歡類似 React.cloneElement 頂層 API,除了偏好以外,也有一個難以規避的問題:在使用 React.cloneElement 擴充 props 時,如果出現 props 命名衝突怎麼辦?
比如一個 input 遇見了命名為 value 的 prop,後果可想而知。
那麼問題來了,是否有更優雅高效的方法解決上述問題?或者,是否有更好的方式,實現更靈活的設計?
答案一定是有的,我將會留在下一篇文章進行講解。
本文源於:How To Master Advanced React Design Patterns,部分內容有改動。
廣告時間: 如果你對前端發展,尤其對 React 技術棧感興趣:我的新書中,也許有你想看到的內容。關注作者 Lucas HC,新書出版將會有送書活動。
Happy Coding!
PS: 作者 Github倉庫 和 知乎問答連結 歡迎各種形式交流!
我的其他幾篇關於React技術棧的文章:
從setState promise化的探討 體會React團隊設計思想