[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} </> ) }