如何掌握高級React設計模式: 複合組件【譯】
來自專欄 IMWeb前端社區11 人贊了文章
英文原文鏈接:How To Master Advanced React Design Patterns: Compound Components
翻譯作者:IMWeb 團隊成員 - howenhuo
為了慶祝 React 16.3 的正式發布,我決定分享我最近使用的一些技術,這些技術徹底改變了我創建 React 組件的方法。因此,我能夠設計出完全可重用的組件,並且可以在許多不同的環境中靈活地使用這些組件。
CodeSandbox
上面的 sandbox 是一個簡潔的 Stepper 組件的初始代碼,我將使用它來展示其中的一些技術。 就目前而言,這個組件完全正常工作,並且完全按照設計目的進行,但它缺乏靈活性。
import React, { Component } from react;import Stepper from "./Stepper"class App extends Component {render() { return ( <Stepper stage={1}/> );}}export default App;
如上可視,Stepper 組件的靈活性在 stage 屬性處終止;我們只能修改 stage 的值來確定 Stepper 組件進行到哪一步。
- 如果我需要將進度塊放在右側怎麼辦?
- 如果我需要一個類似的追加額外 stage 的 Stepper 怎麼辦?
- 如果我需要更改 stage 的內容怎麼辦?
- 如果我想改變 stage 的順序怎麼辦?
就目前而言,我要實現這些變化的唯一方法是完全重寫組件,以相同的方式重寫一個類似的組件。 但是,如果將來又要進行其他更改,那該組件又一次需要重寫。 因此,讓我們嘗試不同的方法來重寫組件,使其具有靈活性和可重用性,以應變將來任何的配置。
在本系列的第一部分中,我們將探討一種名為「複合組件」的設計模式
使用複合組件設計模式
首先,讓我們來看看 Stepper 組件。
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> );}}export default Stepper;
Stepper 組件有一個存儲當前 stage 的狀態對象,一個增加 stage 屬性值的方法,以及一個 render 方法,它返回包含2個子組件的div。
目前,我們明確地將 Progress 和 Steps 組件直接放在 Stepper 組件中。 為了減少這種靜態寫法,我們可以使用 props 對象動態插入子組件。
在 Stepper.js 文件中使用 props.children 對象替換 Progress 和 Steps 組件,並將它們放在 App.js中的 Stepper 組件內。
只需這簡單的改變就給我們帶來很大的收益。現在我們可以選擇組件樹中的哪個組件先渲染; 我們可以選擇進度塊是在左側還是右側。
但這種方法有一個問題: Progress 和 Steps 組件不能再訪問 stage 和 handleClick 屬性了。 為了讓每個子組件獲取它們需要的屬性,我們需要手動遍歷每個子組件並向其注入這些屬性。 我們可以使用 react API 提供的一些輔助方法來實現。 兩個方法是: Children.map() 和 cloneElement()。
const children = React.Children.map(this.props.children, child => {return React.cloneElement(child, {stage, handleClick: this.handleClick})})
Children.map() 類似於 Array.map() 方法。但請務必使用Children.map(),因為 children.props 具有不透明的數據結構,使得 Array.map() 方法不適合此用例。
cloneElement 如名稱一樣,它克隆這些子組件並可以注入額外的屬性,最後返回新的組件。
// Render method of Stepper.jsconst { 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>);
現在我們可以將 Progress 和 Stage 作為子項添加到 Stepper 組件中,運行效果不變。但此時我們可以決定每個組件的位置,甚至可以在左右兩側同時設置進度塊。
import React, { Component } from react;import Stepper from "./Stepper"import Progress from ./components/Progress;import Steps from ./components/Steps;class App extends Component {render() { return ( <div> <Stepper stage={1}> <Progress /> <Steps /> </Stepper> </div> );}}export default App;
靜態屬性
另外一種能夠提高可讀性和易用性的技術就是使用類的靜態屬性。它允許我們直接在類上調用方法。
這是什麼意思? 讓我們來看看…。
首先,我們在 Stepper 組件中創建兩個靜態方法,並將 Progress 和 Steps 組件賦值給它們:
static Progress = Progressstatic Steps = Steps
那麼現在,我們不需要再到處引入 Progress 和 Steps 組件,而是直接從 Stepper 組件中引用它們:
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;
到目前為止,我們已經創建了一個簡單可讀且靈活的API。那麼是不是可以對 Progress 組件使用相同的技術呢? 讓我們來看看......
export default class Progress extends Component {render(){ const {stage} = this.props return( <div style={styles.progressContainer}> <Stage stage={stage} num={1} /> <Stage stage={stage} num={2} /> <Stage stage={stage} num={3} /> <Stage stage={stage} num={4} /> </div> )}}
您也許已經注意到 Progress 組件與之前的 Stepper 組件存在同樣的問題。所以我們用 props.children 對象來替換這 4 個 Stage 組件並遍歷子項添加所需的屬性,然後在 Stepper 類中添加一個 Stage 靜態方法,供外部直接引用 Stage 。
export default class Progress extends Component {render(){ const {stage} = this.props const children = React.Children.map(this.props.children, child => { return React.cloneElement(child, {stage}) }) return( <div stylex={styles.progressContainer}> {children} </div> )}}
完成上述步驟後,我們可以在任意位置動態添加任意數量的 Stage 組件:
import React, { Component } from react;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.Stage num={4} /> </Stepper.Progress> <Stepper.Steps /> </Stepper> );}}export default App;
接下來我們可以對 Steps 組件做同樣的改進,但這個有點不同,因為每個子項都要被 Reacts Transition Group 的 Transition 組件包裹。同樣是使用 Children.map() 遍歷,但只有 Steps 組件的 stage 屬性與子組件的 num 屬性匹配時才展示該子組件。 即它們匹配時,子組件會被包裹在 Transition 組件中(ReactTransitionGroup文檔解釋了此目的)並在屏幕上渲染。
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> );}}?export default Steps;
通過在 Stepper 組件上添加相應的靜態方法,我們可以按照我們想要的順序添加任意數量的Step組件。
import React, { Component } from react;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> );}}export default App;
我們用一種方式就創建了非常靈活可重用的組件。現在我們可以選擇多少個 stage,每個 stage 的文本和順序,以及我們可以決定進度條在左側還是右側。
CodeSandbox雖然改進了很多,但在靈活性上我們仍然受到限制! 如果我們想在 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> );}}export default App;
上面這樣做會破壞我們應用程序的結構,因為 Stepper.Steps 組件不再是 Stepper 組件的直接子組件,所以它無法訪問 stage 屬性了。
這就是為什麼 React 16.3 的發布非常重要! 到目前為止,React』s context API 還處於試驗階段,但現在它已經正式發布了!
在本系列的第2部分中,我將探討如何實現 context API 以便能夠在組件樹中的任何位置傳遞屬性,這樣無論 Stepper.Steps 組件位於何處,它始終都能夠訪問 stage 屬性。
推薦閱讀:
※計算機論文精選-20180719
※設計模式奏鳴曲(一):開篇
※Spring框架中的設計模式(五)
※Render Prop
※從零學習Spring MVC框架「環境搭建和MVC架構」