Use Cases (Diperbarui: 2/6/2026)

Jotai atoms dengan Claude Code: panduan React praktis

Rancang Jotai atoms dengan Claude Code: derived, async, SSR, Provider, tests, dan prompt aman.

Jotai atoms dengan Claude Code: panduan React praktis

Meminta Claude Code “tambahkan Jotai untuk state management” terlalu luas. Kode mungkin berjalan, tetapi ukuran atom, batas dengan server state, posisi Provider, hidrasi SSR, dan cakupan test akan terbentuk secara kebetulan. Ketika filter, draft form, async read, dan UI halaman detail bertemu, keputusan yang samar itu menjadi mahal.

Artikel ini memperlakukan Jotai atoms sebagai alat desain untuk state React yang kecil, bukan sebagai global store serbaguna. Dokumentasi resmi Jotai atom menjelaskan bahwa atom config hanyalah definisi dan tidak menyimpan nilai; nilai hidup di store. Untuk async, rujukannya adalah Jotai async guide dan referensi React untuk <Suspense>. Untuk Provider dan SSR, dasarnya adalah Jotai Provider dan SSR utilities.

Di sisi Claude Code, overview resmi menjelaskan alat yang dapat membaca codebase, mengedit file, dan menjalankan command. Karena itu prompt harus punya scope, dan file sensitif perlu dikecualikan denganpermissions.deny dari Claude Code settings. Untuk konteks React, lihat panduan React Claude Code. Untuk data server, bandingkan dengan panduan TanStack Query.

Tetapkan mental model atom

Atom bukan nilai itu sendiri. Atom adalah definisi stabil yang memberi tahu Jotai cara membaca atau menulis nilai di store. Jika Claude Code tidak diberi konteks ini, ia sering membuatpageStateAtombesar berisi search text, API response, selected rows, toast, dan draft form. Awalnya tampak rapi, tetapi setiap perubahan punya dampak terlalu luas.

Sebelum menulis kode, jawab tiga pertanyaan. Apakah nilai ini UI state atau kebenaran server? Apakah dipakai oleh komponen yang berjauhan atau hanya lokal? Dapatkah nilainya diturunkan dari atom lain? Search text, filter, active tab, toast singkat, dan draft form multi-step cocok untuk Jotai. Product list, user table, auth token, payment session, dan fakta inventory lebih cocok di server-state layer atau server.

StateCocok untuk JotaiHindari
Draft formDibagi antar stepMenyimpan order penuh yang sudah persisted
Filter adminDipakai table, count, dan URLMenyimpan API response lengkap
Modal/toastDibuka dari tombol jauhLog panjang atau audit data
PreferensiTheme, density, dismissed hintToken, email, alamat, data payment

Kegagalan nyata Masa di layar admin adalah menjadikan “pakai Jotai” sebagai tujuan, bukan membuat inventaris state. Versi pertama menyimpan filter, rows yang diambil, selected rows, saving state, dan toast dalam satu atom. Setelah kembali ke list, selection lama memengaruhi bulk action baru. Memisahkan server data, selection state, dan UI singkat membuat diff Claude Code lebih kecil dan review lebih jelas.

Install dan buat slice minimal

Pada app React Vite atau Next.js, install Jotai terlebih dahulu. Jika membutuhkan atom family di kode baru, pilihjotai-family, karena dokumentasi Jotai saat ini menandaiatomFamily darijotai/utils sebagai deprecated untuk Jotai v3.

npm i jotai jotai-family
npm i -D vitest @testing-library/react @testing-library/user-event

TaskBoard berikut kecil tetapi lengkap. Ia memuat primitive atom, derived atom, dan write-only atom dalam contoh React yang bisa dicopy.

import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";

export type TaskStatus = "todo" | "doing" | "done";

export type Task = {
  id: string;
  title: string;
  status: TaskStatus;
};

const createId = () =>
  globalThis.crypto?.randomUUID?.() ?? String(Date.now());

export const tasksAtom = atom<Task[]>([
  { id: "task-1", title: "Write release note", status: "todo" },
]);

export const filterAtom = atom<TaskStatus | "all">("all");
export const draftTitleAtom = atom("");

export const visibleTasksAtom = atom((get) => {
  const filter = get(filterAtom);
  const tasks = get(tasksAtom);
  return filter === "all"
    ? tasks
    : tasks.filter((task) => task.status === filter);
});

export const taskStatsAtom = atom((get) => {
  const tasks = get(tasksAtom);
  return {
    total: tasks.length,
    done: tasks.filter((task) => task.status === "done").length,
  };
});

export const addTaskAtom = atom(null, (get, set) => {
  const title = get(draftTitleAtom).trim();
  if (!title) return;

  set(tasksAtom, (tasks) => [
    ...tasks,
    { id: createId(), title, status: "todo" },
  ]);
  set(draftTitleAtom, "");
});

export const toggleTaskAtom = atom(null, (_get, set, id: string) => {
  set(tasksAtom, (tasks) =>
    tasks.map((task) =>
      task.id === id
        ? { ...task, status: task.status === "done" ? "todo" : "done" }
        : task,
    ),
  );
});

export function TaskBoard() {
  const [draft, setDraft] = useAtom(draftTitleAtom);
  const [filter, setFilter] = useAtom(filterAtom);
  const tasks = useAtomValue(visibleTasksAtom);
  const stats = useAtomValue(taskStatsAtom);
  const addTask = useSetAtom(addTaskAtom);
  const toggleTask = useSetAtom(toggleTaskAtom);

  return (
    <section>
      <p>
        Total: {stats.total} / Done: {stats.done}
      </p>

      <label>
        New task
        <input
          value={draft}
          onChange={(event) => setDraft(event.currentTarget.value)}
        />
      </label>
      <button type="button" onClick={addTask}>
        Add
      </button>

      <select
        value={filter}
        onChange={(event) =>
          setFilter(event.currentTarget.value as TaskStatus | "all")
        }
      >
        <option value="all">All</option>
        <option value="todo">Todo</option>
        <option value="doing">Doing</option>
        <option value="done">Done</option>
      </select>

      <ul>
        {tasks.map((task) => (
          <li key={task.id}>
            <span>{task.title}</span>
            <button
              type="button"
              aria-label={`Mark ${task.title} done`}
              onClick={() => toggleTask(task.id)}
            >
              {task.status === "done" ? "Undo" : "Done"}
            </button>
          </li>
        ))}
      </ul>
    </section>
  );
}

Setelah Claude Code membuat slice seperti ini, minta review pada tiga hal: derived atom tidak menyimpan nilai duplikat, write-only atom mengumpulkan aturan update, dan komponen hanya subscribe atom yang benar-benar dirender.

Use case konkret untuk granularitas

Use case pertama adalah filter admin. search, status, page, dansort dapat berada di atoms ketika table, count, dan URL sync membutuhkannya. API response lengkap tidak boleh berada di atom yang sama. Tulis di prompt: “pisahkan kondisi yang terlihat di URL dari state khusus UI, dan simpan API response di server-state layer.”

Use case kedua adalah checkout atau onboarding multi-step. Field draft, step saat ini, dan validation cocok untuk Jotai. Submitted order, payment session, dan inventory check tidak cocok. Write-only reset atom setelah sukses lebih aman daripada reset field dari banyak komponen.

Use case ketiga adalah UI state pada halaman detail. Expanded rows, active tab, selected ID, dan short toast queue dapat dipisah menjadi atoms kecil agar hanya komponen terkait yang re-render. SatudetailPageAtomlebih mudah dibuat, tetapi lebih sulit diuji dan di-review.

Use case keempat adalah preferensi pengguna. Theme, density, dan dismissed hint dapat memakai storage helper. Data privat tidak boleh dipersist di browser. Pada halaman dengan monetization CTA, state open CTA berisiko rendah, tetapi email pembeli dan riwayat kupon harus di server.

Derived atom dan write-only atom

Derived atom menghitung nilai dari atom lain. Total, list yang difilter, dan hasil validasi biasanya dihitung, bukan disimpan. Write-only atom mengumpulkan action seperti patch, reset, atau normalisasi sebelum save.

import { atom } from "jotai";

export type CheckoutDraft = {
  email: string;
  postalCode: string;
  agreed: boolean;
};

const emptyCheckoutDraft: CheckoutDraft = {
  email: "",
  postalCode: "",
  agreed: false,
};

export const checkoutDraftAtom = atom<CheckoutDraft>(emptyCheckoutDraft);

export const checkoutErrorsAtom = atom((get) => {
  const draft = get(checkoutDraftAtom);
  const errors: Partial<Record<keyof CheckoutDraft, string>> = {};

  if (!draft.email.includes("@")) {
    errors.email = "Check the email address";
  }

  if (!/^\d{3}-?\d{4}$/.test(draft.postalCode)) {
    errors.postalCode = "Enter a seven digit postal code";
  }

  if (!draft.agreed) {
    errors.agreed = "Agreement is required";
  }

  return errors;
});

export const patchCheckoutDraftAtom = atom(
  null,
  (_get, set, patch: Partial<CheckoutDraft>) => {
    set(checkoutDraftAtom, (draft) => ({ ...draft, ...patch }));
  },
);

export const resetCheckoutDraftAtom = atom(null, (_get, set) => {
  set(checkoutDraftAtom, emptyCheckoutDraft);
});

Kegagalan umum adalah menyimpan hasilcheckoutErrorsAtomdi atom lain. Draft berubah, tetapi snapshot error tertinggal. Aturan untuk Claude Code harus jelas: “jangan simpan nilai yang bisa diturunkan dari atoms saat ini.”

Async atom dan batas server state

Async atom berguna, tetapi bukan pengganti seluruh strategi data fetching. Async read atom Jotai dapat mengembalikan Promise, dan Suspense dapat menampilkan fallback saat memuat. Ini cocok untuk read kecil di satu area UI.

import { Suspense } from "react";
import { atom, useAtomValue, useSetAtom } from "jotai";

type Profile = {
  id: string;
  name: string;
  plan: "free" | "pro";
};

export const profileIdAtom = atom("masa");

export const profileAtom = atom(async (get, { signal }) => {
  const id = get(profileIdAtom);
  const response = await fetch(`/api/profiles/${id}`, { signal });

  if (!response.ok) {
    throw new Error("Failed to load profile");
  }

  return (await response.json()) as Profile;
});

function ProfileCard() {
  const profile = useAtomValue(profileAtom);
  return <p>{profile.name} is on the {profile.plan} plan.</p>;
}

function ProfileSwitcher() {
  const setProfileId = useSetAtom(profileIdAtom);
  return (
    <button type="button" onClick={() => setProfileId("demo")}>
      Load demo user
    </button>
  );
}

export function ProfilePanel() {
  return (
    <>
      <ProfileSwitcher />
      <Suspense fallback={<p>Loading profile...</p>}>
        <ProfileCard />
      </Suspense>
    </>
  );
}

Jika butuh retry, stale time, pagination, optimistic mutation, atau invalidation, gunakan server-state library. Jotai boleh menyimpan request parameter dan local UI state; cache response tetap di luar atoms.

Atom family, SSR, dan Provider

Atom family berguna ketika setiap row atau tab memerlukan UI state sendiri. Risikonya adalah cache membesar. Dokumentasi resmi menjelaskan bahwa family menyimpan atoms per parameter; parameter tak terbatas tanpa cleanup dapat menyebabkan memory leak. Untuk kode baru gunakanjotai-family.

import { atom } from "jotai";
import { atomFamily } from "jotai-family";

type RowUi = {
  expanded: boolean;
  selected: boolean;
};

export const rowUiFamily = atomFamily((id: string) =>
  atom<RowUi>({ expanded: false, selected: false }),
);

rowUiFamily.setShouldRemove((createdAt) => {
  return Date.now() - createdAt > 10 * 60_000;
});

export const removeRowUiAtom = atom(null, (_get, _set, id: string) => {
  rowUiFamily.remove(id);
});

Pada SSR, Provider adalah batas penting. Jotai bisa berjalan tanpa Provider eksplisit, tetapi nilai awal per request, isolasi subtree, dan test lebih jelas dengan Provider. Di Next.js App Router,useHydrateAtoms harus berada di client component.

"use client";

import { type PropsWithChildren } from "react";
import { Provider } from "jotai";
import { useHydrateAtoms } from "jotai/utils";
import { tasksAtom, type Task } from "./TaskBoard";

type Props = PropsWithChildren<{
  initialTasks: Task[];
}>;

function HydrateAtoms({ initialTasks, children }: Props) {
  useHydrateAtoms(new Map([[tasksAtom, initialTasks]]));
  return children;
}

export function JotaiRequestProvider(props: Props) {
  return (
    <Provider>
      <HydrateAtoms initialTasks={props.initialTasks}>
        {props.children}
      </HydrateAtoms>
    </Provider>
  );
}

Kesalahan yang sering terjadi adalah mencoba hydrate store yang sama berkali-kali saat user atau tenant berubah. Secara default, hidrasi adalah langkah awal per store. Untuk switching nyata, remount Provider dengan key baru atau buat reset action eksplisit.

Tests dan prompt aman

Jotai Testing guide menyarankan test seperti cara pengguna berinteraksi dengan komponen dan memperlakukan Jotai sebagai detail implementasi. Untuk menerima output Claude Code, cover input, click, hasil terlihat, dan reset.

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Provider } from "jotai";
import { describe, expect, it } from "vitest";
import { TaskBoard } from "./TaskBoard";

describe("TaskBoard", () => {
  it("adds and completes a task", async () => {
    const user = userEvent.setup();

    render(
      <Provider>
        <TaskBoard />
      </Provider>,
    );

    await user.type(screen.getByLabelText("New task"), "Review atoms");
    await user.click(screen.getByRole("button", { name: "Add" }));

    expect(screen.getByText("Review atoms").textContent).toBe("Review atoms");

    await user.click(
      screen.getByRole("button", { name: "Mark Review atoms done" }),
    );

    expect(screen.getByText(/Done: 1/).textContent).toContain("Done: 1");
  });
});

Gunakan prompt yang sempit:

Baca layar React + TypeScript yang ada dan rapikan state dengan Jotai v2.
Hanya edit file di bawah src/features/tasks.
Jangan simpan API response di atoms.
Gunakan atoms hanya untuk UI state dan draft form.
Tambahkan derived atoms, write-only atoms, Provider boundary, dan Vitest tests.
Jika atom family diperlukan, gunakan jotai-family dan tambahkan cleanup.
Akhiri dengan review kritis tentang failure mode, render risk, dan SSR risk.

Kecualikan secrets juga melalui settings:

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)",
      "Read(./build)"
    ]
  }
}

CTA monetisasi dan catatan verifikasi

Artikel Jotai tidak boleh berhenti di snippet. Pembaca butuh aturan keputusan: state apa masuk atoms, apa masuk TanStack Query, dan apa harus tetap di server. Untuk menata prompt Claude Code,CLAUDE.md, aturan review, dan bukti test, mulai darichecklist Claude Code gratis, lalu bandingkanZustand,TanStack Query, dantesting strategies.

Masa menguji pola ini pada layar React kecil. Perbaikan terbesar terjadi sebelum atom ditulis. Prompt pertama mencampur API response, draft form, dan toast dalam satu atom. Prompt kedua menulis “server data di luar atoms”, “derived values tidak disimpan”, dan “Provider plus tests wajib”. Diff mengecil, Vitest mencakup add, complete, dan reset, lalu review menemukan dua risiko yang mudah terlewat: cleanup atom family dan batas hidrasi SSR.

#Claude Code #Jotai #React #state management #atoms
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.