Architecture
React Query (state/cache) -> Zeus api layer (type-safe) -> GraphQL server
api/client.ts,api/query.ts,api/mutation.ts— type-safe GraphQL communication- React Query manages caching, loading/error states, invalidation
- Components use
useQuery/useMutationwhich call Zeus internally
API Layer
// api/client.ts
import { Chain } from '../zeus/index';
export const createChain = () =>
Chain('/graphql', { headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin' });
// api/query.ts
import { createChain } from './client';
export const query = () => createChain()('query');
// api/mutation.ts
import { createChain } from './client';
export const mutation = () => createChain()('mutation');
Selectors
import { Selector, type FromSelector } from '../zeus/index.js';
const postSelector = Selector('Post')({ _id: true, title: true, content: true, published: true });
type PostType = FromSelector<typeof postSelector, 'Post'>; // derive type — never duplicate manually
- 1 file only → define locally
- 2+ files → define in
api/selectors.ts, re-export fromapi/index.ts
$ Variables
Use $ when values come from user input or props:
import { $ } from '../zeus/index';
await mutation()(
{ login: [{ email: $('email', 'String!'), password: $('password', 'String!') }, true] },
{ variables: { email, password } },
);
useQuery + Zeus
⚠️ Use
isLoading, NOTisPending.isPendingistruewhenenabled: false, causing permanent loading states.isLoading(isPending && isFetching) only fires during actual fetches.
import { useQuery } from '@tanstack/react-query';
import { query } from '../api';
import { queryKeys } from '../lib/queryKeys';
import { useAuth } from '../hooks';
const { isAuthenticated } = useAuth();
const { data, isLoading, error } = useQuery({
queryKey: queryKeys.posts, // define in queryKeys.ts first
queryFn: async () => {
const data = await query()({ user: { posts: { _id: true, title: true } } });
return data.user?.posts ?? [];
},
enabled: isAuthenticated,
});
useMutation + Zeus
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { mutation } from '../api';
import { queryKeys } from '@/lib/queryKeys.js';
const queryClient = useQueryClient();
const createPost = useMutation({
mutationFn: async (input: { title: string; content: string }) => {
await mutation()({ user: { createPost: [input, true] } });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.posts });
},
});
Cache Invalidation
- After mutations:
queryClient.invalidateQueries({ queryKey: queryKeys.posts }) - Auth mutation exception (
login/register): forqueryKeys.me, run explicitawait queryClient.fetchQuery({ queryKey: queryKeys.me, queryFn: ... })sync after success; do not rely on invalidation alone - From subscription callbacks: same
invalidateQueriescall - On logout:
queryClient.clear()(clears ALL cache — security)
Key Rules
- ALWAYS use React Query in components — never
useState/useEffectfetching - Zeus
query()/mutation()go insidequeryFn/mutationFnonly - Use
enabledfor conditional queries (e.g.,enabled: isAuthenticated) - ALWAYS define Selectors for reusable query shapes
- ALWAYS use
FromSelectorto derive types — never duplicate backend types manually - Selector in 1 file → local; 2+ files →
api/selectors.ts - Use
$for GraphQL variables from user input or props - One hook per domain — owns queries, mutations, loading/error state; components stay presentational
- Import from
../api— never directly from Zeus - ALWAYS use
queryKeysfrom@/lib/queryKeys.js— never hardcode query key strings like['me']or['todos'] - Keep guest optimization for
mequery (enabledgating), but treat auth mutations as an exception and explicitlyfetchQuery(queryKeys.me)to sync authenticated state deterministically
Converted and distributed by TomeVault — claim your Tome and manage your conversions.