ReactのHooksの中でも、特に難しいとされるのがuseEffectです。
useEffectを使用することで、レンダリングの直前や直後、または指定された状態が変更されたときに副作用を実行できます。
しかし、使い方を誤るとバグやパフォーマンスの問題を引き起こすことがあります。
例えば、以下のようなコードがあるとします。
【useEffectなし】
const Parent = () => {
const [value, setValue] = useState("initial value");
return (
<div>
<Child1 value={value} setValue={setValue} />
<Child2 value={value} />
</div>
);
}
const Child1 = ({ value, setValue }) => {
const onClick = () => {
setValue(0)
}
return (
<>
<div>1つ目 {value}</div>
<button onClick={()=> onClick()}>Button!</button>
</>
);
}
const Child2 = (props) => {
const [aliasValue, setAliasValue] = useState(props.value);
return <div>2つ目 {aliasValue}</div>;
}
このコードでは、ParentコンポーネントがChild1とChild2を含んでいます。
Child1では、ボタンをクリックするとsetValueが呼び出され、valueが0に変更されます。
Child2では、props.valueをaliasValueという名前の状態にコピーしています。
しかし、ボタンをクリックしても、valueを初期値に設定しているaliasValueの値が変わりません。
これは、Reactがprops.valueが変更されたことを検知できていないためです。
なぜかというと、ReactのuseStateの初期値は画面のマウント時に決定されます。
Reactの再レンダリングはライフサイクル上その後に行われるため、マウント時に決定された初期値は不変だからです。
処理順としては、下記の画像の通りになっています。
状態の変更がされた際、再レンダリングされますがアンマウントの処理は走りません。
この問題を解決するために、keyに毎回変わる値を指定してみましょう。
const Parent = () => {
const [value, setValue] = useState("initial value");
// 初期値を変更したいコンポーネントのkeyに必ず毎回変わる値を入れる
return (
<div>
<Child1 value={value} setValue={setValue} />
<Child2 value={value} key={new Date()} />
</div>
);
}
const Child1 = ({ value, setValue }) => {
const onClick = () => {
setValue(0)
}
return (
<>
<div>1つ目 {value}</div>
<button onClick={()=> onClick()}>Button!</button>
</>
);
}
const Child2 = (props) => {
const [aliasValue, setAliasValue] = useState(props.value);
return <div>2つ目 {aliasValue}</div>;
}
キーが更新されると、Reactは変更されたキーに基づいて要素を再配置し、必要に応じて要素を作成、更新、または削除します。
・レンダリング時にkeyの中身を常に変更
・親の仮想DOMが更新される
・子の仮想DOMが更新される
上記のような挙動で、Child2の初期値を変更することができました!
※keyの中身が常に前の値を別の値になるように、Dateインスタンスを使っています。
Q. 仮想DOMってなんですか?
A. ウェブページやアプリの表示内容を変更できます。
仮想DOMは、表示内容に違いが出た部分のみ更新可能です。
さて、このようなコードは適切ではないです。
みんな大好き、useEffectを使った以下のコードをご覧ください。
const Parent = () => {
const [value, setValue] = useState("initial value");
return (
<div>
<Child1 value={value} setValue={setValue} />
<Child2 value={value} />
</div>
);
}
const Child1 = ({ value, setValue }) => {
const onClick = () => {
setValue("new value")
}
return (
<>
<div>1つ目 {value}</div>
<button onClick={onClick}>Button!</button>
</>
);
}
const Child2 = (props) => {
const [aliasValue, setAliasValue] = useState(props.value);
useEffect(() => {
setAliasValue(props.value);
}, [props.value]);
return <div>2つ目 {aliasValue}</div>;
}
はい、我々がよく見ているuseEffectの使い方ですね。
上記のようにuseEffectの依存配列で指定したprops.valueの値が変わった時に、useEffect中の処理でprops.valueの変更をaliasValueに反映することができました。
このように、useEffectを使用することで、コンポーネント内の状態を更新するために必要な追加処理を実行することができます。
また、useEffectはコンポーネント内で実行することができる副作用の管理にも役立ちます。
例えば、APIからデータを取得してコンポーネント内で表示する場合にuseEffectを使用して取得処理を実行し、取得したデータをコンポーネント内で表示することができます。
ただ、useEffectは便利な反面、扱い方によっては思わぬ副作用をもたらすこともあります。
例えば、無限ループが発生する可能性があるため、useEffectの中で再度useEffectを呼び出すことは避けるべきです。
また、useEffectの引数に依存する値を指定することで、依存する値が変更された場合にのみuseEffectを実行することができます。
このように、useEffectがもたらす力は大きいです。
エンジニア自身が設計して、適切なユースケースで使っていく必要があります。