- Published on
React Component Design - Pattern Summary
- Authors
- Name
- Alex Yu
讓我們來綜合整理一下介紹過的 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 的寫法。