Data fetching
How server functions, TanStack Query, and hooks compose - the client-side data pattern applications use.
Data flows one way in Africa OS: server functions fetch or mutate, TanStack Query caches, hooks expose a clean API, and components render. This page documents the full client-side data pattern.
The flow
Server functions as the data layer
Server functions are the only way components talk to the backend (see TanStack Start). They validate input, run an Effect program through the request runtime, and return serializable data or a folded error code.
Query hooks
Hooks wrap server functions in TanStack Query, giving components a clean surface. The retail useProducts hook is the template (packages/applications/retail/src/features/products/hooks/useProducts.ts):
export function useProducts(scope?: ProductsScope) {
const queryClient = useQueryClient();
const organizationSlug = scope?.organizationSlug;
const productsKey = ["retail", "products", organizationSlug ?? "active"];
const { data: products, isLoading, error, refetch } = useQuery({
queryKey: productsKey,
queryFn: () => listProductsFn({ data: { organizationSlug } }),
retry: false,
});
const createProduct = useMutation({
mutationFn: (input) => createProductFn({ data: { ...input, organizationSlug } }),
onSuccess: (result) => {
if ("product" in result) {
void queryClient.invalidateQueries({ queryKey: productsKey });
}
},
});
return { products: products ?? [], isLoading, error, refetch, createProduct };
}
The conventions
The organization slug is part of the query key. ["retail", "products", organizationSlug ?? "active"] - switching organizations refetches the correct catalog. Never build a query key without the tenant context.
Mutations invalidate the list on success. Each mutation checks the result shape ("product" in result) before invalidating, so only successful writes refresh the cache.
Failures are folded, not thrown. Server functions return { product } | { error }. Hooks check the success branch and ignore error codes (or surface them) without a thrown exception.
retry: false on reads. Server data is already authenticated and validated; retrying a 500 from the server is usually pointless.
Data defaults to a safe empty state. products: products ?? [] keeps components renderable while loading.
The QueryClient
Both the shell and application packages export a getQueryClient() (packages/applications/shell/src/queryClient.ts):
- Browser - a stable, memoized client shared across the session.
- Server (SSR) - a fresh client per request, so cached data is never shared across users during server rendering.
- Defaults -
staleTime: 60_000.
The application route tree
Application packages export their internal route tree (src/routes/tree.ts) so hosts can compose routes with their own loaders, while the mounted application resolves the same pages from the URL. Both paths render identical pages.
The rule
Client components never query the database. They call server functions, which run through the request runtime. Tenancy, authentication, and authorization stay server-side by construction.
Next steps
- Retail reference - this pattern in full.
- Adding a feature - build a feature with this data flow.