一键导入
jotai-x
Use when working with Jotai X stores (createAtomStore), accessing state in components or callbacks, persisting state to cookies or localStorage
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when working with Jotai X stores (createAtomStore), accessing state in components or callbacks, persisting state to cookies or localStorage
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Open a concise GitHub follow-up for reusable browser-use limitations. Use when browser automation is blocked by a likely tool-side issue that is worth fixing separately, especially for clicks, dropdowns, file inputs, focus traps, or other repeatable agent/browser failures.
Work heavyweight framework or library tasks with planning-first research, selective deep analysis, and rigorous handoff
Work a task end-to-end with lean context gathering, implementation, and verification
Sync `zbeyens/convex-better-auth` with upstream, then sync kitcn against upstream `convex-better-auth` changes. Use when asked to run `sync-convex-auth`, compare the fork with upstream, fast-forward or PR the fork update when safe, audit commits the fork was behind on, classify relevance to kitcn auth integration, and delegate one implementation PR through `task`.
Audit newer Convex npm releases against kitcn. Use when checking whether a newer `convex` version unlocks kitcn improvements, compatibility work, CLI/agent workflows, or cleanup of local Convex hacks. Reads `https://ship.convex.dev/`, the upstream Convex changelog, and GitHub diffs before delegating an implementation PR through `task`.
Read every doc in www and packages/kitcn/skills/kitcn, sync to active changeset(s), and track with checkmarks.
| description | Use when working with Jotai X stores (createAtomStore), accessing state in components or callbacks, persisting state to cookies or localStorage |
| name | jotai-x |
| metadata | {"skiller":{"source":".agents/rules/jotai-x.mdc"}} |
Minimize subscriptions, maximize performance. Use hooks (useValue) only when you need reactivity. Use store methods (store.get, store.set) everywhere else.
createAtomStore)useAppValue vs store.get, useAppSet vs store.set// ✅ Subscribe with hooks ONLY when component needs to re-render on change
const count = useAppValue("count"); // Component re-renders when count changes
// ✅ Read without subscribing in callbacks/handlers
const handleClick = () => {
const count = store.get("count"); // Just reads current value, no subscription
console.log(count);
};
// ✅ Write with store.set in callbacks/handlers
const handleIncrement = () => {
store.set("count", (prev) => prev + 1);
};
Reading State:
useAppValue('key')store.get('key')store.get('key')store.get('key') unless you need dependencyWriting State:
store.set('key', value)Getting both read + write:
useAppState or useAppSet - use combinations above| Scenario | Use | Don't Use |
|---|---|---|
| Component needs reactivity | useAppValue('key') | store.get('key') |
| Callback reads once | store.get('key') | useAppValue('key') |
| Event handler reads | store.get('key') | useAppValue('key') |
| Any write operation | store.set('key', val) | useAppSet('key') |
| Need both read + write | useAppValue + store.set | useAppState |
import { createAtomStore } from "jotai-x";
export const {
useChatStore, // Get store instance
useChatValue, // Subscribe to value (use sparingly!)
ChatProvider, // Provider component
} = createAtomStore(
{
count: 0,
name: "Alice",
items: [] as string[],
},
{
name: "chat", // Prefix for all hooks
}
);
const store = useChatStore();
// Read without subscribing
store.get("count"); // Get single value
store.getCount(); // Alternative syntax
// Write
store.set("count", 5); // Set value
store.set("count", (c) => c + 1); // Update with function
store.setCount(5); // Alternative syntax
// Subscribe (rarely needed - prefer useAppValue)
const unsub = store.subscribe("count", (value) => {
console.log(value);
});
// Subscribe to value (component re-renders on change)
const count = useChatValue("count");
// With selector
const firstItem = useChatValue(
"items",
{
selector: (items) => items[0],
},
[]
);
// With equality function
const items = useChatValue(
"items",
{
equalityFn: (a, b) => a.length === b.length,
},
[]
);
import { createAtomStore } from "jotai-x";
// 1. Create store
export const { useChatStore, useChatValue, ChatProvider } = createAtomStore(
{
messages: [] as Message[],
input: "",
status: "idle" as "idle" | "loading",
},
{
name: "chat",
}
);
// 2. Custom hook for complex operations
export const useSendMessage = () => {
const store = useChatStore();
return async () => {
// ✅ Use store.get to read without subscribing
const input = store.get("input");
const messages = store.get("messages");
if (!input.trim()) return;
// ✅ Use store.set to write
store.set("status", "loading");
store.set("input", "");
try {
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ input, messages }),
});
const newMessage = await response.json();
// ✅ Functional update
store.set("messages", (prev) => [...prev, newMessage]);
store.set("status", "idle");
} catch (error) {
store.set("status", "idle");
console.error(error);
}
};
};
// 3. Component with minimal subscriptions
function ChatMessages() {
// ✅ Subscribe ONLY to what component needs to render
const messages = useChatValue("messages");
const status = useChatValue("status");
return (
<div>
{messages.map((msg) => (
<Message key={msg.id} {...msg} />
))}
{status === "loading" && <LoadingSpinner />}
</div>
);
}
// 4. Component with event handlers
function ChatInput() {
const store = useChatStore();
const sendMessage = useSendMessage();
// ✅ Subscribe to input for controlled input
const input = useChatValue("input");
return (
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage();
}}
>
<input
value={input}
onChange={(e) => {
// ✅ Use store.set directly
store.set("input", e.target.value);
}}
/>
<button
type="submit"
onClick={() => {
// ✅ Can read current value without subscribing
if (store.get("status") === "loading") {
return; // Don't submit while loading
}
}}
>
Send
</button>
</form>
);
}
// ❌ WRONG - Creates subscription for no reason
function Component() {
const count = useChatValue("count"); // Subscribes!
const handleClick = () => {
console.log(count); // Uses stale closure
};
}
// ✅ CORRECT - Read when needed
function Component() {
const store = useChatStore();
const handleClick = () => {
console.log(store.get("count")); // Always current
};
}
// ❌ WRONG - Over-subscribing
const [count, setCount] = useChatState("count");
// ✅ CORRECT - Subscribe only if component needs reactivity
const count = useChatValue("count");
const store = useChatStore();
// Then use store.set('count', ...) to write
// ❌ WRONG - Unnecessary hook call
const setCount = useChatSet("count");
setCount(5);
// ✅ CORRECT - Direct access
const store = useChatStore();
store.set("count", 5);
// ❌ WRONG - Component subscribes to everything
function Component() {
const input = useChatValue("input");
const status = useChatValue("status");
const messages = useChatValue("messages");
const error = useChatValue("error");
// But only renders messages and status
return (
<div>
{messages.map((m) => (
<div key={m.id}>{m.text}</div>
))}
{status === "loading" && <Spinner />}
</div>
);
}
// ✅ CORRECT - Subscribe only to what you render
function Component() {
const messages = useChatValue("messages");
const status = useChatValue("status");
// Access input/error with store.get when needed
}
| Excuse | Reality |
|---|---|
| "README shows useAppValue, so I should use it" | README shows all features. Use store.get/set for better performance. |
| "Hooks are the React way" | Hooks for reactivity. Store methods for one-time access. Both are React patterns. |
| "store.get/set is more advanced" | Actually simpler - direct access without subscription overhead. |
| "Using hooks everywhere is more consistent" | Consistency at the cost of performance. Each tool for its purpose. |
| "Official docs recommend useAppValue" | Docs show options. Choose based on whether you need subscription. |
| "I need the setter from useAppState" | Use store.set instead - works everywhere, not just in components. |
| "I'll optimize later, hooks for now" | Wrong pattern from start = tech debt. Use store.get/set from beginning. |
| "Getting store adds extra line of code" | One line enables all direct access. More efficient than multiple hooks. |
| "What if I need value multiple times?" | store.get is instant, no overhead. Call it as many times as needed. |
| "Hooks auto-subscribe, don't have to think" | Thinking prevents bugs. Explicit > implicit. Performance > convenience. |
If you see any of these patterns, reconsider:
useAppState anywhere (almost never needed)useAppSet anywhere (use store.set)useAppValue in a callback/event handler (use store.get)useAppValue calls in one component (over-subscribing)useAppValue but never using the returned value in JSX (just use store.get)All of these mean: Switch to store.get or store.set.
Bad pattern (unnecessary subscriptions):
function UserProfile() {
const firstName = useChatValue("firstName");
const lastName = useChatValue("lastName");
const email = useChatValue("email");
const age = useChatValue("age");
// Component re-renders on ANY change to firstName, lastName, email, OR age
return (
<div>
{firstName} {lastName}
</div>
);
}
// Subscribes to 4 atoms, only uses 2 in render
Good pattern (minimal subscriptions):
function UserProfile() {
// Only subscribe to what we render
const firstName = useChatValue("firstName");
const lastName = useChatValue("lastName");
// Access others with store.get when needed in callbacks
const store = useChatStore();
const handleSubmit = () => {
const email = store.get("email");
const age = store.get("age");
// ...
};
return (
<div>
{firstName} {lastName}
</div>
);
}
// Subscribes to 2 atoms, exactly what we render
// Custom hook with derived state
export const useChatEmpty = () => {
// ✅ Subscribe only to values used in computation
const status = useChatValue("status");
const messages = useChatValue("messages");
const hasSubmitted = useChatValue("hasSubmitted");
return !hasSubmitted && messages.length === 0 && status === "ready";
};
export const useSwitchChat = () => {
const store = useChatStore();
const currentChatId = useChatValue("chatId");
return (chatId: string) => {
if (chatId === currentChatId) return;
// ✅ Multiple writes with store.set
store.set("chatId", chatId);
store.set("newChatId", null);
store.set("status", "syncing");
store.get("actions").setMessages([]);
store.set("hasSubmitted", false);
};
};
export const useRetry = () => {
const store = useChatStore();
// ✅ Subscribe only if we need reactivity for computation
const messages = useChatValue("messages");
return async (messageId?: string) => {
const currentIdx = messageId
? messages.findIndex((m) => m.id === messageId)
: messages.findLastIndex((m) => m.role === "assistant");
// ✅ Read values with store.get - no subscription needed
const chatId = store.get("chatId");
const actions = store.get("actions");
// Perform async operation...
await deleteMessages(chatId, messageId);
// ✅ Write with store.set
const newMessages = messages.slice(0, currentIdx);
actions.setMessages(newMessages);
actions.regenerate();
};
};
Persist store values to cookies or localStorage by using atomWithCookie or atomWithLocalStorage directly in store definition.
| Storage | Use Case |
|---|---|
| Cookie | Server-side access needed, SSR hydration, small data |
| LocalStorage | Client-only, larger data, no SSR needed |
1. Store definition - Replace primitive with atomWithCookie:
import { atomWithCookie } from "@/lib/utils/atomWithCookie";
export const { useChatStore, useChatValue, ChatProvider } = createAtomStore(
{
isOpen: atomWithCookie("chat_isOpen", true), // ✅ Direct assignment
// ... other state
},
{ name: "chat" }
);
2. Server-side hydration - Read cookie and pass to Provider:
// Layout.tsx (Server Component)
import { cookies } from "next/headers";
import { getCookieParser } from "@/lib/utils/getCookieParser";
export async function Layout({ children }) {
const cookieStore = await cookies();
const parser = getCookieParser(cookieStore);
const isOpen = parser.boolean("chat_isOpen") ?? true;
return <ChatProvider initialValues={{ isOpen }}>{children}</ChatProvider>;
}
Cookie naming convention: prefix_key (e.g., chat_isOpen, chat_mode)
import { atomWithLocalStorage } from "@/lib/utils/atomWithLocalStorage";
export const { useAppStore, useAppValue, AppProvider } = createAtomStore(
{
theme: atomWithLocalStorage("app_theme", "light"),
// ... other state
},
{ name: "app" }
);
No server hydration needed - localStorage is client-only.
// ❌ WRONG - Don't use extend option for persistence
createAtomStore(
{ isOpen: true },
{
name: "chat",
extend: (atoms) => ({ ...atoms, isOpen: someAtom }), // Unnecessary complexity
}
);
// ✅ CORRECT - Direct assignment
createAtomStore({ isOpen: atomWithCookie("chat_isOpen", true) }, { name: "chat" });
// ❌ WRONG - Forgetting server hydration with cookies
<ChatProvider initialValues={{}}> // Cookie value ignored on first render
// ✅ CORRECT - Pass server-read value
<ChatProvider initialValues={{ isOpen }}> // Hydrated from cookie