React Component Design - Pattern Summary
- 發佈時間
總結#
讓我們來綜合整理一下介紹過的pattern
Compound Component&Provider/Context#
Compound Component
跟Provider/Context
都將state
保留在自身的component
內,並且提供定義好的介面跟元件給外部使用,讓使用方式跟元件符合一定程度的上下文語意,而且內部元件的render
順序可以讓外部決定。Compound Component
為了解決prop drilling
的問題通常會搭配Provider/Context
使用,並且可以增加一些機制避免元件在錯誤的地方被使用,或者透過檢查children
的方式讓不在定義介面內的children component
也可以正常render
。
已複製!function Usage({ onToggle = (...args) => console.log('onToggle', ...args), }) { return ( <Toggle onToggle={onToggle}> <Toggle.On>The button is on</Toggle.On> <Toggle.Off>The button is off</Toggle.Off> <div> <Toggle.Button /> </div> </Toggle> ) }
Render Props#
將想要傳給children
的props
傳給children
做render
,state
跟一部分的function
還是保留在自身的component
內,換言之,只保留最小限度的邏輯在自身component
內,給外部最大程度的自由度去render
想要的東西。
已複製!function Usage({ onToggle = (...args) => console.log('onToggle', ...args), }) { return ( <Toggle onToggle={onToggle}> {({ on, toggle }) => ( <div> {on ? 'The button is on' : 'The button is off'} <Switch on={on} onClick={toggle} /> <hr /> <button aria-label="custom-button" onClick={toggle}> {on ? 'on' : 'off'} </button> </div> )} </Toggle> ) }
Prop Collections and Getters#
Prop Collections
用意在集結一些共用的props
,Prop Getters
用意在改寫Prop Collections
提供出來的props
,Prop Collections
跟Prop Getters
通常搭配一起使用,保留給外部使用者有可以客製化的方式。
已複製!function Usage({ onToggle = (...args) => console.log('onToggle', ...args), onButtonClick = () => console.log('onButtonClick'), }) { return ( <Toggle onToggle={onToggle}> {({ on, getTogglerProps }) => ( <div> <Switch {...getTogglerProps({on})} /> <hr /> <button {...getTogglerProps({ 'aria-label': 'custom-button', onClick: onButtonClick, id: 'custom-button-id', })} > {on ? 'on' : 'off'} </button> </div> )} </Toggle> ) }
State Initializers and Reducer#
State Initializers
讓外部使用者可以重新reset state
到一開始的狀態。State Reducer
則是讓外部使用者決定action
觸發時要執行的邏輯,在自身的component
內還是會有自身的state
,但如果外部使用者有傳入state reducer
時則會讓外部使用者決定最終的state
,並且傳回給內部component
做最終的state
更新。
已複製!function Usage({ initialOn = false, onToggle = (...args) => console.log('onToggle', ...args), onReset = (...args) => console.log('onReset', ...args), }) { return ( <Toggle initialOn={initialOn} onToggle={onToggle} onReset={onReset} > {({ getTogglerProps, on, reset }) => ( <div> <Switch {...getTogglerProps({on})} /> <hr /> <button onClick={() => reset()}>Reset</button> </div> )} </Toggle> ) }
已複製!class Usage extends React.Component { static defaultProps = { onToggle: (...args) => console.log('onToggle', ...args), onReset: (...args) => console.log('onReset', ...args), } initialState = { timesClicked: 0 } state = this.initialState handleToggle = (...args) => { this.setState(({ timesClicked }) => ({ timesClicked: timesClicked + 1, })) this.props.onToggle(...args) } handleReset = (...args) => { this.setState(this.initialState) this.props.onReset(...args) } toggleStateReducer = (state, changes) => { if (changes.type === 'forced') { return changes } if (this.state.timesClicked >= 4) { return {...changes, on: false} } return changes } render() { const { timesClicked } = this.state return ( <Toggle stateReducer={this.toggleStateReducer} onToggle={this.handleToggle} onReset={this.handleReset} ref={this.props.toggleRef} > {({ on, toggle, reset, getTogglerProps }) => ( <div> <Switch {...getTogglerProps({ on: on, })} /> {timesClicked > 4 ? ( <div data-testid="notice"> Whoa, you clicked too much! <br /> <button onClick={() => toggle({type: 'forced'})}> Force Toggle </button> <br /> </div> ) : timesClicked > 0 ? ( <div data-testid="click-count"> Click count: {timesClicked} </div> ) : null} <button onClick={reset}>Reset</button> </div> )} </Toggle> ) } }
Control Props#
Control Props
在自身的component
保留自身的state
,若外部有傳入value
時,則state的控制權便交給外部使用者。通常在自身component
的state
改變時也會需要通知外部使用者,讓外部使用者有可以決定最終value
變化的權利。
已複製!class Usage extends React.Component { state = { bothOn: false } lastWasButton = false handleStateChange = changes => { const isButtonChange = changes.type === Toggle.stateChangeTypes.toggleOn || changes.type === Toggle.stateChangeTypes.toggleOff if ( changes.type === Toggle.stateChangeTypes.toggle || (this.lastWasButton && isButtonChange) ) { this.setState({ bothOn: changes.on }) this.lastWasButton = false } else { this.lastWasButton = isButtonChange } } render() { const { bothOn } = this.state const { toggle1Ref, toggle2Ref } = this.props return ( <div> <Toggle on={bothOn} onStateChange={this.handleStateChange} ref={toggle1Ref} /> <Toggle on={bothOn} onStateChange={this.handleStateChange} ref={toggle2Ref} /> </div> ) } }
Higher-Order Component#
Higher-Order Component
簡稱HOC
,通常是將一些共用的邏輯放在此處,介面是傳入一個component
,回傳一個新的component
。但這種方式有一些缺點,而且在現今React版本已過時,可以選擇用Render Props
或hook
方式取代。
已複製!function withToggle(Component) { function Wrapper(props, ref) { return ( <Toggle.Consumer> {toggleContext => ( <Component toggle={toggleContext} {...props} ref={ref} /> )} </Toggle.Consumer> ) } Wrapper.displayName = `withToggle(${Component.displayName || Component.name})` return hoistNonReactStatics(React.forwardRef(Wrapper), Component) } const Layer1 = () => <Layer2 /> const Layer2 = withToggle(({ toggle: { on }}) => ( <Fragment> {on ? 'The button is on' : 'The button is off'} <Layer3 /> </Fragment> )) const Layer3 = () => <Layer4 /> const Layer4 = withToggle(({ toggle: { on, toggle }}) => ( <Switch on={on} onClick={toggle} /> )) function Usage({ onToggle = (...args) => console.log('onToggle', ...args), }) { return ( <Toggle onToggle={onToggle}> <Layer1 /> </Toggle> ) }
延伸問題#
HOC
vsRender Props
vsHooks
最後想介紹一個網站:https://www.patterns.dev/ 裡面介紹了更多種的design pattern,包含如何用js實現一般常見的pattern,以及React跟Vue相關的patterns,有興趣的人可在參考,但請謹記pattern是用在適合的場景下才採取的一種策略,並不是一定要將code 改成 pattern的寫法。