- Published on
React Component Design - Control Props
- Authors
- Name
- Alex Yu
一般來說,常見的Form
元件: <select>
, <input>
, <textarea>
,當使用者在輸入時,通常改變的是元件內部他們自己的state
。但在React的世界裡,state
的改變通常是透過setState
的做法,確保single source of truth。為此,可以把state
傳給Form
的元件value props
,如下圖,透過這樣的方式控制value
,並且要傳入change handler props
去改變value
,不然在Form
元件輸入後value
不會有任何改變。
讓我們將之前的Toggle
元件用Controlled Component
的方式改寫
class Toggle extends React.Component {
state = { on: false };
isControlled(prop) {
return this.props[prop] !== undefined;
}
getState() {
return {
on: this.isControlled('on') ? this.props.on : this.state.on,
};
}
toggle = () => {
if (this.isControlled('on')) {
this.props.onToggle(!this.getState().on);
} else {
this.setState(
({ on }) => ({ on: !on }),
() => {
this.props.onToggle(this.getState().on);
}
);
}
};
render() {
return <Switch on={this.getState().on} onClick={this.toggle} />;
}
}
class Usage extends React.Component {
state = { bothOn: false };
handleToggle = (on) => {
this.setState({ bothOn: on });
};
render() {
const { bothOn } = this.state;
const { toggle1Ref, toggle2Ref } = this.props;
return (
<div>
<Toggle on={bothOn} onToggle={this.handleToggle} ref={toggle1Ref} />
<Toggle on={bothOn} onToggle={this.handleToggle} ref={toggle2Ref} />
</div>
);
}
}
改寫 getState
上面這樣的方式適用於state
只有on
的時候,如果有多個state
的時候就會顯得有點冗長
我們可以改寫上面的getState
成
class Toggle extends React.Component {
// same as before
getState() {
return Object.entries(this.state).reduce((combinedState, [key, value]) => {
if (this.isControlled(key)) {
combinedState[key] = this.props[key];
} else {
combinedState[key] = value;
}
return combinedState;
}, {});
}
// same as before
}
監聽 Toggle State
如果我們希望有一個function handler
可以去監聽Toggle
的state
,
換言之,Toggle
的state
改變時,可以被上層使用Toggle
元件的地方接收到訊息,讓介面長得像下方
class Usage extends React.Component {
state = { bothOn: false };
handleStateChange = ({ on }) => {
this.setState({ bothOn: on });
};
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>
);
}
}
class Toggle extends React.Component {
static defaultProps = {
onToggle: () => {},
onStateChange: () => {},
};
state = { on: false };
isControlled(prop) {
return this.props[prop] !== undefined;
}
// 將state做為參數傳入,確保可以拿到當下state的值
getState(state = this.state) {
return Object.entries(state).reduce((combinedState, [key, value]) => {
if (this.isControlled(key)) {
combinedState[key] = this.props[key];
} else {
combinedState[key] = value;
}
return combinedState;
}, {});
}
// Toggle內setState的行為應該都透過這個function去執行
internalSetState(changes, callback) {
let allChanges;
this.setState(
(state) => {
const combinedState = this.getState(state);
const changesObject =
typeof changes === 'function' ? changes(combinedState) : changes;
allChanges = changesObject;
const nonControlledChanges = Object.entries(changesObject).reduce(
(newChanges, [key, value]) => {
if (!this.isControlled(key)) {
newChanges[key] = value;
}
return newChanges;
},
{}
);
return Object.keys(nonControlledChanges).length
? nonControlledChanges
: null;
},
() => {
this.props.onStateChange(allChanges);
callback();
}
);
}
toggle = () => {
this.internalSetState(
({ on }) => ({ on: !on }),
() => {
this.props.onToggle(this.getState().on);
}
);
};
render() {
return <Switch on={this.getState().on} onClick={this.toggle} />;
}
}
改寫 internalSetState
internalSetState
在上面的例子雖然有把狀態傳出去給上層,但上層卻無法得知是什麼樣的事件觸發了internalSetState
,這邊我們可以利用跟stateReducer
類似的技巧,在觸發internalSetState
的時候傳入type
,這樣上層便可以得知是什麼樣的事件觸發 state change
class Toggle extends React.Component {
static stateChangeTypes = {
toggle: '__toggle__',
toggleOn: '__toggle_on__',
toggleOff: '__toggle_off__',
};
// same as before
internalSetState(changes, callback = () => {}) {
let allChanges;
this.setState(
(state) => {
const combinedState = this.getState(state);
const changesObject =
typeof changes === 'function' ? changes(combinedState) : changes;
allChanges = changesObject;
// 將type destruct, 避免type被setState進state
const { type: ignoredType, ...onlyChanges } = changesObject;
const nonControlledChanges = Object.entries(onlyChanges).reduce(
(newChanges, [key, value]) => {
if (!this.isControlled(key)) {
newChanges[key] = value;
}
return newChanges;
},
{}
);
return Object.keys(nonControlledChanges).length
? nonControlledChanges
: null;
},
() => {
this.props.onStateChange(allChanges, this.getState());
callback();
}
);
}
toggle = ({ on: newState, type = Toggle.stateChangeTypes.toggle } = {}) => {
this.internalSetState(
({ on }) => ({
on: typeof newState === 'boolean' ? newState : !on,
type,
}),
() => {
this.props.onToggle(this.getState().on);
}
);
};
handleSwitchClick = () => this.toggle();
handleOffClick = () =>
this.toggle({
on: false,
type: Toggle.stateChangeTypes.toggleOff,
});
handleOnClick = () =>
this.toggle({
on: true,
type: Toggle.stateChangeTypes.toggleOn,
});
render() {
return (
<div>
<Switch on={this.getState().on} onClick={this.handleSwitchClick} />
<button onClick={this.handleOffClick}>off</button>
<button onClick={this.handleOnClick}>on</button>
</div>
);
}
}
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>
);
}
}
結論
Control Props
能用在元件在不傳入value
時,仍能保有自身的state
,但如果parent component
想要控制元件的value
時仍然保有一定的彈性,並且能夠讓parent component
監聽自身state
改變時的event handler
。