Jotaiで値が更新されない時の対処方法[React,Jotai,StaleClosure]

jotaiとても便利ですよね

そんな中でuseAtomによって用意したsetterで更新した値が、予想通りの動きをせず困ったことがあったので、その対処法をメモとして残しておこうと思います。

# Jotaiとは
reactの状態管理ライブラリです。

https://jotai.org/

useStateのように一つのコンポーネントで保持する変数とは異なり、useContextのようなReactアプリケーション全体で変数を扱えるようになります。

Jotaiは、atomという単位でstateを取り扱っていきます。

```tsx
import { atom } from 'jotai'

export const sampleAtom = atom(0);
```

そこから使いたいコンポーネントでuseAtomを使うことでgetterとsetterが使えるようになります。
```tsx
import { sampleAtom } from "@/store"
import { useAtom } from "jotai"
const [counter, setCounter] = useAtom(sampleAtom);
```

:::note
### jotaiでの注意点
`use client`を指定しているクライアントコンポーネント間でのみ作動します。
:::

# 起きた現象

useAtomでgetterとsetterを用意して、下のようにsetterで値を更新しながら非同期でconsoleで表示しようとしたとします。

```tsx
import { sampleAtom } from "@/store"
import { useAtom } from "jotai"
import { useEffect } from "react"

export const JotaiSample = () => {
  const [counter, setCounter] = useAtom(sampleAtom);

  // 非同期で処理する
  const asyncLogStaleValue = () => {
    setTimeout(() => {
      // この時点では counter が更新されていても、コンポーネントマウント時の値（0）を参照している可能性がある
      console.log("stale counter:", counter);
    }, 1000);
  };

  useEffect(() => {
    // マウント時にカウンターを更新
    setCounter(prev => prev + 1);
    // 非同期処理を呼ぶ
    asyncLogStaleValue();
  }, []);

  return (
    <div>
      <h2>Counter: {counter}</h2>
      <p>この例では、非同期で読み取っている counter の値が更新されない（stale）状況を再現しています。</p>
    </div>
  );
}
```

ブラウザ上ではcounterの値は1に変わっていたのですが、consoleでは0となっており、更新されていない状態で表示されていました。

# 原因

これはStaleClosureという現象が起きているからでした。

closureは、以下のように説明されます。
>クロージャは、組み合わされた（囲まれた）関数と、その周囲の状態（レキシカル環境）への参照の組み合わせです。言い換えれば、クロージャは関数にその外側のスコープにアクセスする機能を提供します。JavaScript では、クロージャは関数が作成されるたびに、関数作成時点で作成されます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Closures

という説明なのですがわかりづらいですよね。

一般的には、クロージャーとは関数が作成された時点で関数のスコープ内にある変数の値を保持し続けてしまう状態のことを指します。

上のコードで言うとマウントされた状態では、counterは0になっており、作成された`asyncLogStaleValue`はcounterが0の状態を保持しています。

さらに、setCounterでatomの値を変更した後に、非同期で`asyncLogStaleValue`を実行しているため`asyncLogStaleValue`が再作成(レンダリングによる再作成)が行われず、counterは0のまま扱ってしまっています。

このため思ったような動作をしていない状態になっています。

# 解決方法

JotaiにはStoreと呼ばれるのが存在します。

https://jotai.org/docs/core/store

jotaiのatomはデフォルトでStoreを確保してそこにstateを保管しています。
そのStoreはgetDefaultStoreで作成したインスタンスからアクセスすることができます。
```ts
const defaultStore = getDefaultStore()
```

そこからgetメソッドを使うことでstoreの値を直接参照しにいくことができます。
```ts
defaultStore.get(sampleAtom)
```

これを使って下のようにコードを変えてみたところ、思い通りの動作をしました。
動作した理由としては、変数ではなくstoreからatomのstateを取りにいくという関数を実行しているため、StaleClosureによる値の保持が行われず、実行するごとに値が取りにいっているからだと思います。
```tsx
"use client"
import { sampleAtom } from "@/store"
import { useAtom, getDefaultStore } from "jotai"
import { useEffect } from "react"

export const JotaiSample = () => {
  const [counter, setCounter] = useAtom(sampleAtom);
  const store = getDefaultStore(); //デフォルトのstoreを取得


  // 非同期処理の中で、初期の counter 値がクロージャに捕捉される例
  const asyncLogStaleValue = () => {
    setTimeout(() => {
      // この時点では counter が更新されていても、
      console.log("stale counter:", store.get(sampleAtom)); //storeで参照
    }, 1000);
  };

  useEffect(() => {
    // マウント時にカウンターを更新
    setCounter(prev => prev + 1);
    // 非同期処理を呼ぶ
    asyncLogStaleValue();
  }, []);

  return (
    <div>
      <h2>Counter: {counter}</h2>
      <p>この例では、非同期で読み取っている counter の値が更新されない（stale）状況を再現しています。</p>
    </div>
  );
```

これ以外のやり方だとそもそもJotaiを使わずに、useRefでのメモ化を利用することで解決できるそうですが、今回はどうしてもJotaiを使うことで解決する必要があったので採用していません。

ぜひ参考にしてください。