0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-04-04

jotaiとても便利ですよね

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

Jotaiとは

reactの状態管理ライブラリです。

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

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

import { atom } from 'jotai'

export const sampleAtom = atom(0);

そこから使いたいコンポーネントでuseAtomを使うことでgetterとsetterが使えるようになります。

import { sampleAtom } from "@/store"
import { useAtom } from "jotai"
const [counter, setCounter] = useAtom(sampleAtom);

jotaiでの注意点

use clientを指定しているクライアントコンポーネント間でのみ作動します。

起きた現象

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

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と呼ばれるのが存在します。

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

const defaultStore = getDefaultStore()

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

defaultStore.get(sampleAtom)

これを使って下のようにコードを変えてみたところ、思い通りの動作をしました。
動作した理由としては、変数ではなくstoreからatomのstateを取りにいくという関数を実行しているため、StaleClosureによる値の保持が行われず、実行するごとに値が取りにいっているからだと思います。

"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を使うことで解決する必要があったので採用していません。

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

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?