一.componentWillReceiveProps に対する誤解
componentWillReceiveProps は通常 propsWillChange と考えられており、実際に我们也通过它来判断 props change。しかし、実際には componentWillReceiveProps は毎回 rerender 時に呼び出されます。props が変わったかどうかに関わらず:
class A extends React.Component {
render() {
return <div>Hello {this.props.name}</div>;
}
componentWillReceiveProps(nextProps) {
console.log('Running A.componentWillReceiveProps()');
}
}
class B extends React.Component {
constructor() {
super();
this.state = { counter: 0 };
}
render() {
return <A name="World" />
}
componentDidMount() {
setInterval(() => {
this.setState({
counter: this.state.counter + 1
});
}, 1000)
}
}
ReactDOM.render(<B/>, document.getElementById('container'));
上記の例では、親コンポーネント B の state change が子コンポーネント A の render 及び componentWillReceiveProps の呼び出しを引き起こしましたが、A では props change は発生していません
そうです、新しい props を受け取れば、componentWillReceiveProps は呼び出されます。たとえ新しい props が古いものと完全に同じであっても:
UNSAFE_componentWillReceiveProps() is invoked before a mounted component receives new props.
Note that if a parent component causes your component to re-render, this method will be called even if props have not changed.
関連実装は以下の通り:
updateComponent: function () {
var willReceive = false;
var nextContext;
if (this._context !== nextUnmaskedContext) {
nextContext = this._processContext(nextUnmaskedContext);
willReceive = true;
}
// Not a simple state update but a props update
if (prevParentElement !== nextParentElement) {
willReceive = true;
}
if (willReceive && inst.componentWillReceiveProps) {
inst.componentWillReceiveProps(nextProps, nextContext);
}
}
(典藏版 ReactDOM v15.6.1 より抜粋)
つまり、componentWillReceiveProps の呼び出しタイミングは:
現在のコンポーネントの更新を引き起こす && (context が変化 || 親コンポーネントの render 結果が変化、つまり現在のコンポーネントが rerender が必要)
注意してください、ここでは props に対する diff は行われていません:
React doesn't make an attempt to diff props for user-defined components so it doesn't know whether you've changed them.
props の値には制約がなく、diff が難しいため:
Oftentimes a prop is a complex object or function that's hard or impossible to diff, so we call it always (and rerender always) when a parent component rerenders.
唯一保証できるのは props change が必ず componentWillReceiveProps をトリガーすることですが、その逆は真ではありません:
The only guarantee is that it will be called if props change.
P.S.より多くの関連議論は Documentation for componentWillReceiveProps() is confusing を参照
二.getDerivedStateFromProps の理解方法
getDerivedStateFromProps は componentWillReceiveProps を置き換えるためのもので、state が props の変化に関連するシナリオに対応します:
getDerivedStateFromProps exists for only one purpose. It enables a component to update its internal state as the result of changes in props.
つまり props の変化が state の変化を引き起こすことを許可します(これを derived state、つまり派生 stateと呼びます)。ほとんどの場合 props 値を state に詰める必要はありませんが、一部のシナリオでは避けられません。例えば:
-
現在のスクロール方向を記録する(recording the current scroll direction based on a changing offset prop)
-
propsを取ってリクエストを送信する(loading external data specified by a source prop)
これらのシナリオの特徴は props の変化に関連し、新旧 props を取得して比較/計算する必要があることです。
componentWillReceiveProps と同様に、getDerivedStateFromProps も props change 時のみトリガーされるわけではありません。具体的には、そのトリガータイミングは:
With React 16.4.0 the expected behavior is for getDerivedStateFromProps to fire in all cases before shouldComponentUpdate.
更新フローにおいて、shouldComponentUpdate の前に呼び出されます。つまり、更新フローに入れば(更新理由が props change でも state change でも)、getDerivedStateFromProps がトリガーされます。
具体的な実装としては、nextContext の計算(nextContext = this._processContext(nextUnmaskedContext))と同様に、更新が必要かどうか(shouldComponentUpdate)を確定する前に、まず nextState を計算する必要があります:
export function applyDerivedStateFromProps(
workInProgress: Fiber,
ctor: any,
getDerivedStateFromProps: (props: any, state: any) => any,
nextProps: any,
) {
const prevState = workInProgress.memoizedState;
const partialState = getDerivedStateFromProps(nextProps, prevState);
// Merge the partial state and the previous state.
const memoizedState =
partialState === null || partialState === undefined
? prevState
: Object.assign({}, prevState, partialState);
workInProgress.memoizedState = memoizedState;
// Once the update queue is empty, persist the derived state onto the
// base state.
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null && workInProgress.expirationTime === NoWork) {
updateQueue.baseState = memoizedState;
}
}
(react/packages/react-reconciler/src/ReactFiberClassComponent.js より抜粋)
getDerivedStateFromProps は nextState を計算する必須环节となりました:
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates.
function mountIndeterminateComponent(
current,
workInProgress,
Component,
renderExpirationTime,
) {
workInProgress.tag = ClassComponent;
workInProgress.memoizedState =
value.state !== null && value.state !== undefined ? value.state : null;
const getDerivedStateFromProps = Component.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
Component,
getDerivedStateFromProps,
props,
);
}
adoptClassInstance(workInProgress, value);
mountClassInstance(workInProgress, Component, props, renderExpirationTime);
// render を呼び出し、第一段階終了
return finishClassComponent(
current,
workInProgress,
Component,
true,
hasContext,
renderExpirationTime,
);
}
(react/packages/react-reconciler/src/ReactFiberBeginWork.js より抜粋)
したがって、初回レンダリング時にも呼び出されます。これが componentWillReceiveProps との最大の違いです。
三。派生 state の実践原則
派生 state を実装するには 2 つの方法があります:
-
getDerivedStateFromProps:propsから部分stateを派生させ、その戻り値は現在のstateにmergeされます -
componentWillReceiveProps:このライフサイクル関数内でsetStateする
実際のアプリケーションでは、2 つの一般的なシナリオで問題が発生しやすい(anti-pattern、つまりアンチパターンと呼ばれる):
-
props変化時に無条件でstateを更新する -
state内にキャッシュされたpropsを更新する
componentWillReceiveProps 時に無条件で state を更新すると、setState() で手動更新した state が上書きされ、予期しない状態丢失が発生します:
When the source prop changes, the loading state should always be overridden. Conversely, the state is overridden only when the prop changes and is otherwise managed by the component.
例えば(componentWillReceiveProps を例として挙げますが、getDerivedStateFromProps も同様):
class EmailInput extends Component {
state = { email: this.props.email };
render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}
handleChange = event => {
this.setState({ email: event.target.value });
};
componentWillReceiveProps(nextProps) {
// This will erase any local state updates!
// Do not do this.
this.setState({ email: nextProps.email });
}
}
上記の例では、ユーザーが input 制御に文字列を入力し(手動で state を更新)、この時親コンポーネントの更新が該コンポーネントの rerender を引き起こすと、ユーザーが入力した内容は nextProps.email に上書きされ(オンライン Demo を参照)、状態丢失が発生します。
この問題に対して、私たちは通常このように解決します:
class EmailInput extends Component {
state = {
email: this.props.email
};
componentWillReceiveProps(nextProps) {
// Any time props.email changes, update state.
if (nextProps.email !== this.props.email) {
this.setState({
email: nextProps.email
});
}
}
}
props change を email に正確に限定し、もはや無条件に state をリセットしません。完璧に見えますが、本当にそうでしょうか?
実際にはまだ厄介な問題が存在します。一部の状況では外部から state をリセットする必要があります(例えばパスワード入力をリセット)。state リセット条件を限定した後、親コンポーネントからの props.email 更新は無条件に input 制御に伝達されなくなります。したがって、以前は EmailInput コンポーネントの rerender を引き起こして入力内容を props.email にリセットできましたが、今は機能しません。
したがって、外部から入力内容を props.email にリセットする方法を考える必要があります。多くの方法があります:
-
EmailInputにresetValue()メソッドを提供し、外部からrefを通じて呼び出す -
外部が
EmailInputのkeyを変更し、強制的に新しいEmailInputを再作成して、初期状態にリセットする目的を達成する -
keyの殺傷力が大きすぎる(削除再作成、およびコンポーネント初期化コスト)、または不便(keyはすでに他の用途がある)場合、props.myKeyを追加してcomponentWillReceivePropsと組み合わせて局所状態リセットを実現する
その中で、最初の 방법은 class 形式のコンポーネントにのみ適用可能で、後者 2 つにはこの制限がありません。具体的なシナリオに応じて柔軟に選択できます。第三の方法は少し複雑で、具体的な操作は Alternative 1: Reset uncontrolled component with an ID prop を参照
類似のシナリオが問題を起こしやすい根源は:
when a derived state value is also updated by setState calls, there isn't a single source of truth for the data.
一方では props から state を計算し、他方では手動で setState して更新します。この時該 state には 2 つのソースがあり、コンポーネントデータの単一ソース原則に違反します
この問題を解決する鍵は単一データソースを保証し、不必要なコピーを拒絶することです:
For any piece of data, you need to pick a single component that owns it as the source of truth, and avoid duplicating it in other components.
したがって 2 つの方案があります(データソースを 1 つ削減すればよい):
-
stateを完全に削除し、こうすればstateとpropsの衝突も存在しなくなります -
props change を無視し、初めて传入された props のみをデフォルト値として保持する
2 つの方法とも単一データソースを保証します(前者は props、後者は state)。このようなコンポーネントは完全制御コンポーネントと完全非制御コンポーネントと呼ぶこともできます。
四。「制御」と「非制御」
コンポーネントは制御コンポーネントと非制御コンポーネントに分類されます。同様に、データもこのように理解できます
制御コンポーネントと非制御コンポーネント
フォーム入力制御(<input>、<textarea>、<select> など)に対して提案された概念で、意味上の違いは制御コンポーネントのフォームデータは React コンポーネントによって処理され(React コンポーネントに制御され)、非制御コンポーネントのフォームデータは DOM メカニズムに委ねられる(React コンポーネントに制御されない)ことです。
制御コンポーネント は自分自身の状態を維持し、ユーザー入力に応じてこの状態を更新します:
An input form element whose value is controlled by React is called a controlled component. When a user enters data into a controlled component a change event handler is triggered and your code decides whether the input is valid (by re-rendering with the updated value). If you do not re-render then the form element will remain unchanged.
ユーザーが制御コンポーネントと対話する時、ユーザー入力が UI にフィードバックされるかどうかは、change イベントに対応する処理関数に依存します(内部状態を変更する必要があるかどうか、rerender を通じて UI にフィードバック)。ユーザー入力は React コンポーネントに制御されます。例えば:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
// ここで入力を UI にフィードバックするかどうかを決定
this.setState({value: event.target.value});
}
render() {
return (
<input type="text" value={this.state.value} onChange={this.handleChange} />
);
}
}
非制御コンポーネント はそのような状態を維持せず、ユーザー入力は React コンポーネントに制御されません:
An uncontrolled component works like form elements do outside of React. When a user inputs data into a form field (an input box, dropdown, etc) the updated information is reflected without React needing to do anything. However, this also means that you can't force the field to have a certain value.
ユーザーと非制御コンポーネントの対話は React コンポーネントに制御されず、入力は即座に UI にフィードバックされます。例えば:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.input = React.createRef();
}
handleSubmit(event) {
// input の入力は直接 UI にフィードバックされ、必要な時のみ DOM から読み取る
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
データの角度から見た制御と非制御
非制御コンポーネントは DOM をデータソースとします:
An uncontrolled component keeps the source of truth in the DOM.
制御コンポーネントは自身が維持する state をデータソースとします:
Since the value attribute is set on our form element, the displayed value will always be this.state.value, making the React state the source of truth.
プログラムの振る舞いを予測可能にする鍵は変因を減らすことです。つまり唯一データソースを保証することです。するとデータソースが唯一のコンポーネントがあり、これを完全制御コンポーネントと完全非制御コンポーネントと呼びます。
以前の派生 state のシナリオに対応すると、これら 2 つの解決策があります:
// (データ)完全に制御されたコンポーネント。入力 state を維持せず、value は完全に外部に制御される
function EmailInput(props) {
return <input onChange={props.onChange} value={props.email} />;
}
// (データ)完全に非制御のコンポーネント。自分の state のみを維持し、props からのいかなる更新も完全に受け付けない
class EmailInput extends Component {
state = { email: this.props.defaultEmail };
handleChange = event => {
this.setState({ email: event.target.value });
};
render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}
}
P.S.注意、「データ制御コンポーネント」と「制御コンポーネント」は全く異なる 2 つの概念です。制御コンポーネントの定義に従えば、上記の例の 2 つとも制御コンポーネントです。
したがって、props を state にコピーする必要があるシナリオでは、props を受け入れて完全に自分の state とし、外部の影響を受けなくするか(データ制御):
Instead of trying to "mirror" a prop value in state, make the component controlled
あるいは自分の state を捨て、データに対する制御を完全に放棄します:
Remove state from our component entirely.
五。計算結果のキャッシュ
別の時には、props を state にコピーするのは計算結果をキャッシュするためで、重複計算を避けるためです。
例えば、一般的なリスト項目を入力キーワードでフィルタリングするシナリオ:
class Example extends Component {
state = {
filterText: "",
};
static getDerivedStateFromProps(props, state) {
if (
props.list !== state.prevPropsList ||
state.prevFilterText !== state.filterText
) {
return {
prevPropsList: props.list,
prevFilterText: state.filterText,
// props 計算結果を state にキャッシュ
filteredList: props.list.filter(item => item.text.includes(state.filterText))
};
}
return null;
}
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
使用可能ですが、複雑すぎます。getDerivedStateFromProps を通じて別の要因(state.filteredList)を作り出し、こうすると props change と state change が両方ともフィルタ結果に影響を与え、問題を起こしやすくなります。
実際、重複計算を避けたい場合、結果を state にキャッシュする必要はありません。例えば:
class Example extends PureComponent {
state = {
filterText: ""
};
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
const filteredList = this.props.list.filter(
item => item.text.includes(this.state.filterText)
)
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
PureComponent の render() が props change または state change 時のみ再び呼び出される特性を利用して、直接 render() 内で安心して計算を行います。
完璧に見えますが、実際のシナリオの state と props は通常これほど単純ではなく、もし別の計算と無関係な props または state が更新されても rerender が引き起こされ、重複計算が発生します。
したがって、信頼できない PureComponent を捨てて、このように解決します:
import memoize from "memoize-one";
class Example extends Component {
state = { filterText: "" };
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
計算結果を state 内に置かず、rerender も避けず、外部にキャッシュします。こうすれば清潔で信頼できます。
コメントはまだありません