Noh | エンジニア向け情報共有コミュニティ
Signup / Login

[React] Classに渡したsetStateが反映されない

y
yosi2024/09/10 1:17
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が変化しません。なぜでしょうか。

A
Answerer AIPowered by gpt-3.5-turbo

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 の値が更新されることになります。

y
yosi2024/09/10 1:20

初期値の0が使用されているのは正しいと思います。しかし、提案された手法では解決しませんでした。

A
Answerer AIPowered by gpt-3.5-turbo

申し訳ありません、回答に不正確さがありました。

問題の根本原因は、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 の値が正しく反映されるようになります。

y
yosi2024/09/10 1:22

コンストラクタ内で updateUI() を最初に実行しても解決しません。なぜならconstructor実行後にaddEventListenerされることがあるからです。

A
Answerer AIPowered by gpt-3.5-turbo

申し訳ありません、再度誤った情報を提供しました。

問題の解決策として、クロージャーの問題を回避するために 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 の値が正しく変化するはずです。お手数をおかけしましたが、この修正が問題を解決できます。

y
yosi2024/09/10 1:27

参考

https://btj0.com/blog/react/useeffect-state-problem/
y
yosi2024/09/10 1:31
const App: FC = () => { const [counter, setCounter] = useState(0) useEffect(() => { ref.current.addEventListener('mousedown', onMouseDown) }, []); const onMouseDown = () => { setCounter(counter + 1); }; return ( <> {counter} </> ) }

counterが1より増えません。クロージャーの仕様も含めて理由をコード例を含めて詳しく解説してください。

A
Answerer AIPowered by gpt-3.5-turbo

この問題は、useEffect 内で参照される onMouseDown 関数が counter 変数をクロージャーとしてキャプチャしているために発生しています。counter 変数が変更されるごとに、新しい onMouseDown 関数が作成されますが、useEffect では初回のみ一度だけ呼び出されるため、変更後の onMouseDown 関数が登録されません。

具体的なコードの動作は以下のようになります。

  1. useEffect の初回実行時に、onMouseDown 関数が作成され、addEventListener に登録されます。
  2. ユーザーがマウスをクリックすると onMouseDown 関数が呼び出されます。
  3. onMouseDown 関数内で、現在の counter の値に 1 を加算した結果を setCounter に渡します。
  4. setCounter は非同期でスケジュールされ、counter の値が更新されます。
  5. しかし、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} </> ); }

修正後のコードでは以下のようになります。

  1. useEffect 内部で、新しい onMouseDown 関数が作成されます。
  2. useEffect の依存リストに counter を指定することで、counter の値が変更されるたびに useEffect が再実行されます。
  3. useEffect 内部で、前回の onMouseDown 関数がイベントリスナーから正常に削除され、新しい onMouseDown 関数がイベントリスナーに追加されます。
  4. ユーザーがマウスをクリックすると、新しい onMouseDown 関数を介して正しい counter の値が setCounter に渡され、counter の値が更新されます。

これにより、クロージャーの問題を回避して、counter の値を正しく更新できるようになります。

y
yosi2024/09/10 11:40

解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} </> ) }
コメントするにはログインが必要です