Skip to content
Africa OS Knowledgebase
Esc
navigateopen⌘Jpreview
On this page

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

Last updated on August 18, 2026