這一周連續(xù)發(fā)表了兩篇關于 React 的文章:
其中涉及到 React 組件復用烁试、輪子設計相關話題匠楚,并配合相關場景實例進行了分析只锭。這些內容都算是 React 設計模式少梁,一提到 Design Patterns我纪,讀者大可不必恐懼麻顶,事實上這都是 React 開發(fā)應用靈活性的體現(xiàn)巷折。今天這篇文章遍略,我們繼續(xù)通過一個場景惧所,循序漸進,通過一步步優(yōu)化設計來進行加深理解绪杏。
場景介紹
屏幕左側大面積展現(xiàn)區(qū)塊內容下愈,點擊 continue 按鈕,切換為下條內容信息蕾久;右側是一個導航條势似,指示當前區(qū)塊展示信息條目。
如果看 Gif 圖不過癮,可以到 CodeSandbox 進行在線了解履因。
具體代碼結構為:
class App extends Component {
render() {
return (
<Stepper stage={1}/>
);
}
}
Stepper 組件 'stage' prop 表示默認開始第幾個區(qū)塊障簿,同時具用同名 'stage' 狀態(tài)。stage 在這里表示左側一個個內容區(qū)塊栅迄。
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 順序該怎么辦余掖?
現(xiàn)有代碼基礎上寸爆,這些問題都可以解決。但是需要重新更改組件編寫內容盐欺。如果某天又新增或者調整了需求赁豆,組件內容同樣又需要改寫。
接下來冗美,我們用另一種方式實現(xiàn)需求魔种,使得代碼更加靈活,復用性更強粉洼。
重新設計
仔細觀察 Stepper 組件:它包含了當前區(qū)塊 stage节预,以及一個更改 stage 的方法,渲染了兩個子組件属韧。
我們使用 Function as Child Component 手段安拟,將 Stepper 組件重構。(如果對 Function as Child Component 不熟悉宵喂,請參考我之前文章 組件復用那些事兒 - React 實現(xiàn)按需加載輪子)
如下圖:
Progress 和 Steps 組件不再直接出現(xiàn)在 Stepper 組件的 render 方法中糠赦。我們使用 this.props.children 對 Stepper 組件的所有子組件進行渲染。這樣 Stepper 組件渲染的內容更加靈活锅棕。
但是僅僅這樣的修改是不可能完成需求的拙泽,當用戶點擊 continue 按鈕,stage 并不會進行切換裸燎。因為 Progress 和 Steps 組件無法再通過 props 感知 stage 和 handleClick 方法顾瞻。
為了解決這個問題,我們可以手動遍歷 Stepper 組件的子節(jié)點顺少,并對相應 props 一一注入。如下代碼:
const children = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {stage, handleClick: this.handleClick})
})
借助 React.Children.map 進行子節(jié)點遍歷,并通過 React.cloneElement 方法對子組件進行拷貝脆炎,這個方法通過第二個參數(shù)梅猿,具有添加額外 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 組件中創(chuàng)建兩個 static 方法弧烤,并賦值給 Progress 組件和 Steps 組件:
static Progress = Progress;
static Steps = Steps
現(xiàn)在忱屑,在 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;
這樣的好處體現(xiàn)在不用一次次地 import 進來 Progress 組件和 Steps 組件,它們都將作為 Stepper 的靜態(tài)屬性出現(xiàn)暇昂。我個人并不是很喜歡這種做法莺戒。
使用 React Transition Group
我們使用 React Transition Group 對 Steps 組件內容添加過渡動畫。只有當 props.num 與 this.props.stage 相等時急波,區(qū)塊內容設置為可見:
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 排列順序等都可以隨意搭配赏寇。
重構代碼以及效果可以訪問這里查看绘盟。
思考及待續(xù)
如果你覺得上述代碼完美無懈可擊,那顯然想簡單了扁耐。需求是變化多端的番捂,如果我們想在 Steps 區(qū)塊上,加一個大標題呢渠退?
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 組件的直接唯一子節(jié)點了碎乃,那預期之中的 props 自然又一次無法取得姊扔!
問題也不僅僅于此。筆者本人不是很喜歡類似 React.cloneElement 頂層 API梅誓,除了偏好以外恰梢,也有一個難以規(guī)避的問題:在使用 React.cloneElement 擴充 props 時佛南,如果出現(xiàn) props 命名沖突怎么辦?
比如一個 <input> 遇見了命名為 value 的 prop嵌言,后果可想而知嗅回。
那么問題來了,是否有更優(yōu)雅高效的方法解決上述問題摧茴?或者绵载,是否有更好的方式,實現(xiàn)更靈活的設計苛白?
答案一定是有的娃豹,我將會留在下一篇文章進行講解。
本文源于:How To Master Advanced React Design Patterns购裙,部分內容有改動懂版。
廣告時間:
如果你對前端發(fā)展,尤其對 React 技術棧感興趣:我的新書中缓窜,也許有你想看到的內容定续。關注作者 Lucas HC,新書出版將會有送書活動禾锤。
Happy Coding!
PS: 作者 Github倉庫 和 知乎問答鏈接 歡迎各種形式交流私股!
我的其他幾篇關于React技術棧的文章:
從setState promise化的探討 體會React團隊設計思想
組件復用那些事兒 - React 實現(xiàn)按需加載輪子
從 React 綁定 this恩掷,看 JS 語言發(fā)展和框架設計