コンポーネントがアンマウントされたときに状態をlocalStorage
に保存したい。これはcomponentWillUnmount
で機能していました。
useEffect
フックでも同じことをしようとしましたが、useEffect
の戻り関数の状態が正しくないようです。
何故ですか?クラスを使用せずに状態を保存するにはどうすればよいですか?
これはダミーの例です。閉じるボタンを押すと、結果は常に0になります。
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function Example() {
const [tab, setTab] = useState(0);
return (
<div>
{tab === 0 && <Content onClose={() => setTab(1)} />}
{tab === 1 && <div>Why is count in console always 0 ?</div>}
</div>
);
}
function Content(props) {
const [count, setCount] = useState(0);
useEffect(() => {
// TODO: Load state from localStorage on mount
return () => {
console.log("count:", count);
};
}, []);
return (
<div>
<p>Day: {count}</p>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => props.onClose()}>close</button>
</div>
);
}
ReactDOM.render(<Example />, document.querySelector("#app"));
UseEffectフックで同じことをしようとしましたが、useEffectのreturn関数の状態が正しくないようです。
この理由は閉鎖によるものです。クロージャは、スコープ内の変数への関数の参照です。 useEffect
コールバックは、コンポーネントがマウントされたときに1回だけ実行されるため、戻りコールバックは初期カウント値0を参照しています。
ここで与えられる答えは、私がお勧めするものです。 [count]
をuseEffect
に渡すという@Jed Richardの回答をお勧めします。これは、カウントが変更されたときにのみlocalStorage
に書き込む効果があります。これは、すべての更新で何も書かないで渡すというアプローチよりも優れています。カウントを非常に頻繁に(数ミリ秒ごとに)変更しない限り、パフォーマンスの問題は発生せず、localStorage
が変更されるたびにcount
に書き込むことは問題ありません。
useEffect(() => { ... }, [count]);
アンマウント時にlocalStorage
への書き込みのみを要求する場合、使用できるいハック/解決策があります-refs。基本的には、コンポーネント内のどこからでも参照できるコンポーネントのライフサイクル全体に存在する変数を作成します。ただし、状態をその値に手動で同期する必要があり、非常に面倒です。 refsはcurrent
フィールドを持つオブジェクトであり、useRef
を複数回呼び出すと同じオブジェクトが返されるため、refsは上記のクロージャの問題を引き起こしません。 .current
値を変更する限り、useEffect
は常に(のみ)最新の値を読み取ることができます。
const {useState, useEffect, useRef} = React;
function Example() {
const [tab, setTab] = useState(0);
return (
<div>
{tab === 0 && <Content onClose={() => setTab(1)} />}
{tab === 1 && <div>Count in console is not always 0</div>}
</div>
);
}
function Content(props) {
const value = useRef(0);
const [count, setCount] = useState(value.current);
useEffect(() => {
return () => {
console.log('count:', value.current);
};
}, []);
return (
<div>
<p>Day: {count}</p>
<button
onClick={() => {
value.current -= 1;
setCount(value.current);
}}
>
-1
</button>
<button
onClick={() => {
value.current += 1;
setCount(value.current);
}}
>
+1
</button>
<button onClick={() => props.onClose()}>close</button>
</div>
);
}
ReactDOM.render(<Example />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
UseEffectコールバック関数は初期カウントを表示しています。これは、useEffectが初期レンダリングで1回だけ実行され、初期レンダリング中に存在したcountの値が0であるコールバックが保存されるためです。
代わりにあなたの場合に行うことは
useEffect(() => {
// TODO: Load state from localStorage on mount
return () => {
console.log("count:", count);
};
});
反応ドキュメントでは、それがこのように定義されている理由の理由を見つけるでしょう
正確にReactエフェクトをクリーンアップしますか?Reactただし、以前に学習したように、エフェクトは1回だけではなく、すべてのレンダリングで実行されます。これが、Reactは、次回のエフェクト実行前に前のレンダリングからエフェクトをクリーンアップする理由です。
Why Effects Run on Each Update
各レンダーで実行されます。最適化するために、count
の変更で実行するようにできます。しかし、これは、ドキュメントにも記載されているuseEffect
の現在提案されている動作であり、実際の実装で変更される可能性があります。
useEffect(() => {
// TODO: Load state from localStorage on mount
return () => {
console.log("count:", count);
};
}, [count]);
他の答えは正しいです。そして、なぜ[count]
をuseEffectに追加し、count
が変更されるたびにlocalStorageに保存しますか?そのようなlocalStorageを呼び出しても、実際のパフォーマンスの低下はありません。
このパターンを試してください:
function Content(props) {
[count, setCount] = useState(0);
// equivalent of componentWillUnmount:
useEffect(() => () => {
console.log('count:', count);
}, []);
// or to have a callback in place every time the state of count changes:
useEffect(() => () => {
console.log('count has changed:', count);
}, [count]);
}
つまり、const/let/varを使用せずに、状態変数とセッターをコンポーネント(関数)のスコープに宣言します。これにより、誤って初期化されるのを防ぎます。
また、useEffectの「関数を返す関数」コード構成体の少し耐えやすい(私の意見では!)にも注意してください。
受け入れられた回答のように状態の変更を手動で追跡する代わりに、useEffectを使用してrefを更新できます。
function Content(props) {
const [count, setCount] = useState(0);
const currentCountRef = useRef(count);
// update the ref if the counter changes
useEffect(() => {
currentCountRef.current = count;
}, [count]);
// use the ref on unmount
useEffect(
() => () => {
console.log("count:", currentCountRef.current);
},
[]
);
return (
<div>
<p>Day: {count}</p>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => props.onClose()}>close</button>
</div>
);
}