State & Data Flow¶
State management patterns using TanStack Query and React context.
State Categories¶
| Category | Technology | Examples |
|---|---|---|
| Server State | TanStack Query | Sessions, chats, embeddings |
| Auth State | BetterAuth | Session, user, organizations |
| UI State | React useState | Form inputs, modals, selections |
| Persisted UI | localStorage | Tutorial progress |
TanStack Query Setup¶
Provider¶
context/tanstack-provider.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false
}
}
});
export function TanstackProvider({ children }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
Usage in Root Layout¶
Query Patterns¶
Standard Query¶
const { data, isLoading, error } = useQuery({
queryKey: ["sessions", organizationSlug],
queryFn: () => fetchJSON("/sessions", token),
enabled: Boolean(token && organizationSlug)
});
Query Key Structure¶
Query keys are arrays for proper cache management:
// Organization-scoped
["sessions", organizationSlug]
["embeddings", organizationSlug, { limit: 100 }]
// Resource-specific
["session", organizationSlug, sessionId]
["chat", organizationSlug, chatId]
// Nested resources
["session-logs", organizationSlug, sessionId]
["session-clips", organizationSlug, sessionId]
Dependent Queries¶
const { data: session } = useQuery({
queryKey: ["session", sessionId],
queryFn: () => fetchSession(sessionId)
});
const { data: logs } = useQuery({
queryKey: ["session-logs", sessionId],
queryFn: () => fetchLogs(sessionId),
enabled: Boolean(session) // Only run when session exists
});
Polling¶
const { data: job } = useQuery({
queryKey: ["training-job", jobId],
queryFn: () => fetchJob(jobId),
refetchInterval: (data) =>
["pending", "running"].includes(data?.status) ? 5000 : false
});
Mutation Patterns¶
Standard Mutation¶
const createSession = useMutation({
mutationFn: (data: CreateSessionData) =>
fetchJSON("/sessions", token, { method: "POST", body: data }),
onSuccess: () => {
toast.success("Session created");
queryClient.invalidateQueries(["sessions", organizationSlug]);
},
onError: (error) => {
toast.error("Failed to create session");
console.error(error);
}
});
Optimistic Updates¶
const sendMessage = useMutation({
mutationFn: (prompt: string) => sendToAPI(prompt),
onMutate: async (prompt) => {
// Cancel outgoing refetches
await queryClient.cancelQueries(["chat", chatId]);
// Snapshot previous value
const previousChat = queryClient.getQueryData(["chat", chatId]);
// Optimistically update
queryClient.setQueryData(["chat", chatId], (old) => ({
...old,
messages: [...old.messages, { role: "user", content: prompt }]
}));
return { previousChat };
},
onError: (err, prompt, context) => {
// Rollback on error
queryClient.setQueryData(["chat", chatId], context.previousChat);
},
onSettled: () => {
queryClient.invalidateQueries(["chat", chatId]);
}
});
Cache Invalidation¶
// Single query
queryClient.invalidateQueries(["sessions", orgSlug]);
// Multiple queries
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === "sessions" ||
query.queryKey[0] === "session"
});
// All queries matching prefix
queryClient.invalidateQueries({ queryKey: ["sessions"] });
Auth State¶
Session Hook¶
// From BetterAuth
const { data: session, isLoading } = authClient.useSession();
// User data
session?.user?.id
session?.user?.email
session?.user?.name
// Session metadata
session?.session?.activeTeamId
Token Management¶
// In useAuth hook
const { data: token } = useQuery({
queryKey: ["auth-token"],
queryFn: async () => {
const result = await authClient.token();
return result?.data?.token;
},
staleTime: 10 * 60 * 1000 // Cache 10 minutes
});
Active Organization¶
const activeOrg = authClient.useActiveOrganization();
// Sync with URL
useEffect(() => {
if (organizationSlug && activeOrg?.slug !== organizationSlug) {
authClient.organization.setActive({ organizationSlug });
}
}, [organizationSlug]);
Context Providers¶
Role Context¶
context/role-provider.tsx
const RoleContext = createContext<{
role: Role | null;
isAdmin: boolean;
isManager: boolean;
isBasic: boolean;
} | null>(null);
export function RoleProvider({ children, requireRole }) {
const [role, setRole] = useState<Role | null>(null);
useEffect(() => {
const fetchRole = async () => {
const result = await authClient.organization.getActiveMemberRole();
setRole(result.data?.role);
};
fetchRole();
}, []);
// Role enforcement logic...
return (
<RoleContext.Provider value={{
role,
isAdmin: role === "admin" || role === "owner",
isManager: role === "owner",
isBasic: role === "member"
}}>
{children}
</RoleContext.Provider>
);
}
Local UI State¶
Component State¶
function SessionLogger() {
const [formData, setFormData] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// ...
}
Cross-Tab Persistence¶
hooks/use-tutorial-progress.ts
function useTutorialProgress(pageId: string, stepIds: string[]) {
const getProgress = () => {
const stored = localStorage.getItem(`${pageId}-tutorial-progress`);
return stored ? JSON.parse(stored) : {};
};
const progress = useSyncExternalStore(
(callback) => {
window.addEventListener("storage", callback);
return () => window.removeEventListener("storage", callback);
},
getProgress,
() => ({}) // Server snapshot
);
const completeStep = (stepId: string) => {
const current = getProgress();
current[stepId] = true;
localStorage.setItem(`${pageId}-tutorial-progress`, JSON.stringify(current));
};
return { progress, completeStep };
}
Data Flow Diagram¶
┌─────────────────────────────────────────────────────────────┐
│ Component │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │
│ │ useAuth() │ │ useSession() │ │ useState() │ │
│ │ (token, user) │ │ (session data) │ │ (UI state) │ │
│ └────────┬────────┘ └────────┬────────┘ └──────────────┘ │
│ │ │ │
└───────────┼────────────────────┼─────────────────────────────┘
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ TanStack Query │ │ BetterAuth │
│ QueryClient │ │ Client │
└─────────┬─────────┘ └─────────┬─────────┘
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ fetchJSON() │ │ Auth API │
│ (REST API) │ │ (BetterAuth) │
└─────────┬─────────┘ └───────────────────┘
│
▼
┌───────────────────┐
│ Core API │
│ /api/v1/actions │
└───────────────────┘
fetchJSON Utility¶
lib/utils.ts
export async function fetchJSON<T>(
path: string,
token: string,
request?: RequestInit,
timeoutMs = 30000
): Promise<T> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
const url = `${API_BASE_URL}${path}`;
const isFormData = request?.body instanceof FormData;
const response = await fetch(url, {
...request,
signal: controller.signal,
headers: {
Authorization: `Bearer ${token}`,
...(isFormData ? {} : { "Content-Type": "application/json" }),
...request?.headers
},
body: isFormData ? request.body : JSON.stringify(request?.body)
});
clearTimeout(timeout);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}