[React] Classに渡したsetStateが反映されない
const MyComponent: FC = () => { const [flagForUpdateUI, setFlagForUpdateUI] = useState<number>(0) const updateUI = () => { console.log("updateUI", flagForUpdateUI) setFlagForUpdateUI(flagForUpdateUI + 1) } const objectRef = useRef<MyClass | null>(null) useEffect(() => { const myClass = new MyClass(updateUI) }) return <></> } class MyClass { updateUI: () => void constructor(updateUI: () => void) { this.updateUI = updateUI window.addEventListener("resize", () => { this.updateUI() }) } }
上記コードでウィンドウがリサイズされてMyClass 内のupdateUI()が実行されてもflagForUpdateUIが変化しません。なぜでしょうか。
MyClass 内で updateUI() を実行しても flagForUpdateUI の値が変化しないのは、クロージャーの仕組みによるものです。
MyClass のコンストラクタ内で window.addEventListener を使用してイベントリスナーを設定しています。このイベントリスナーはクロージャーとして動作し、this.updateUI() 内で参照される updateUI 関数はクロージャーが作成された瞬間のものです。
そのため、updateUI 関数内では flagForUpdateUI + 1 の値が正しく取得されないことになります。実際にはイベントリスナーが呼ばれるたびに、初期値の 0 が使われます。
この問題を解決するには、クロージャーを使わずに最新の flagForUpdateUI の値を正しく取得する必要があります。以下のように修正することで解決できます。
class MyClass { updateUI: () => void constructor(updateUI: () => void) { this.updateUI = updateUI window.addEventListener("resize", () => { this.updateUI() }) // コンストラクタ内で flagForUpdateUI を参照し、最新の値を取得する this.updateUI() } }
これにより、updateUI() 内で flagForUpdateUI は最新の値を正しく反映するようになります。ウィンドウがリサイズされるたびに updateUI が実行され、flagForUpdateUI の値が更新されることになります。
初期値の0が使用されているのは正しいと思います。しかし、提案された手法では解決しませんでした。
申し訳ありません、回答に不正確さがありました。
問題の根本原因は、MyClass のインスタンスが作成されるまで updateUI() が呼び出されないことです。クロージャーの問題ではありません。
MyClass のコンストラクタ内で updateUI() を最初に実行するように修正することで、問題を解決できます。
以下に修正例を示します。
class MyClass { updateUI: () => void constructor(updateUI: () => void) { this.updateUI = updateUI window.addEventListener("resize", () => { this.updateUI() }) // コンストラクタ内で updateUI() を最初に実行する this.updateUI() } }
この修正により、MyClass インスタンスが作成されるときに最初に updateUI() が呼び出されるため、flagForUpdateUI の値が正しく反映されるようになります。
コンストラクタ内で updateUI() を最初に実行しても解決しません。なぜならconstructor実行後にaddEventListenerされることがあるからです。
申し訳ありません、再度誤った情報を提供しました。
問題の解決策として、クロージャーの問題を回避するために flagForUpdateUI を依存リストに追加し、useEffect フック内で MyClass のインスタンスを作成するように修正することをお勧めします。
以下に修正例を示します。
const MyComponent: FC = () => { const [flagForUpdateUI, setFlagForUpdateUI] = useState<number>(0); const updateUI = useCallback(() => { console.log("updateUI", flagForUpdateUI); setFlagForUpdateUI((prev) => prev + 1); }, [flagForUpdateUI]); const objectRef = useRef<MyClass | null>(null); useEffect(() => { objectRef.current = new MyClass(updateUI); return () => { // クリーンアップ時にイベントリスナーを削除する objectRef.current?.removeEventListener(); }; }, [updateUI]); return <></>; };
修正により、updateUI() 関数が正しい flagForUpdateUI の値を参照できるようになります。また、useEffect フックで MyClass のインスタンスを作成し、クリーンアップ時にはイベントリスナーを削除する処理も追加しました。
これにより、ウィンドウがリサイズされるたびに updateUI() が実行され、flagForUpdateUI の値が正しく変化するはずです。お手数をおかけしましたが、この修正が問題を解決できます。
const App: FC = () => { const [counter, setCounter] = useState(0) useEffect(() => { ref.current.addEventListener('mousedown', onMouseDown) }, []); const onMouseDown = () => { setCounter(counter + 1); }; return ( <> {counter} </> ) }
counterが1より増えません。クロージャーの仕様も含めて理由をコード例を含めて詳しく解説してください。
この問題は、useEffect 内で参照される onMouseDown 関数が counter 変数をクロージャーとしてキャプチャしているために発生しています。counter 変数が変更されるごとに、新しい onMouseDown 関数が作成されますが、useEffect では初回のみ一度だけ呼び出されるため、変更後の onMouseDown 関数が登録されません。
具体的なコードの動作は以下のようになります。
useEffectの初回実行時に、onMouseDown関数が作成され、addEventListenerに登録されます。- ユーザーがマウスをクリックすると
onMouseDown関数が呼び出されます。 onMouseDown関数内で、現在のcounterの値に 1 を加算した結果をsetCounterに渡します。setCounterは非同期でスケジュールされ、counterの値が更新されます。- しかし、
useEffectは初回の実行時にのみ呼び出されるため、新しいonMouseDown関数が登録されることはありません。そのため、クリックイベントが発生しても更新されたcounterの値を反映することができません。
この問題を解決するには、useEffect の依存リストに counter を追加することで、counter の値が変更されるたびに useEffect が再実行されるようにします。
以下に修正例を示します。
const App: FC = () => { const [counter, setCounter] = useState(0); useEffect(() => { const onMouseDown = () => { setCounter(prevCounter => prevCounter + 1); }; ref.current.addEventListener('mousedown', onMouseDown); return () => { ref.current.removeEventListener('mousedown', onMouseDown); }; }, [counter]); return ( <> {counter} </> ); }
修正後のコードでは以下のようになります。
useEffect内部で、新しいonMouseDown関数が作成されます。useEffectの依存リストにcounterを指定することで、counterの値が変更されるたびにuseEffectが再実行されます。useEffect内部で、前回のonMouseDown関数がイベントリスナーから正常に削除され、新しいonMouseDown関数がイベントリスナーに追加されます。- ユーザーがマウスをクリックすると、新しい
onMouseDown関数を介して正しいcounterの値がsetCounterに渡され、counterの値が更新されます。
これにより、クロージャーの問題を回避して、counter の値を正しく更新できるようになります。
解1: 最新のStateを参照する
const App: FC = () => { const [counter, setCounter] = useState(0) useEffect(() => { ref.current.addEventListener('mousedown', onMouseDown) }, []); const onMouseDown = () => { setCounter((prev) => prev + 1); }; return ( <> {counter} </> ) }
解2: RandomやUUIDを使用する
const App: FC = () => { const [flagForUpdateUI, setFlagForUpdateUI] = useState(0) useEffect(() => { ref.current.addEventListener('mousedown', onMouseDown) }, []); const onMouseDown = () => { setFlagForUpdateUI(crypto.randomUUID()); }; return ( <> {flagForUpdateUI} </> ) }
