Skip to Content
FrontendPlatformQueries and Mutations Architecture

Queries and Mutations Architecture

This documentation explains how queries and mutations are handled in the codebase using Jotai, React Query, Suspense and hydration with Next.js.

Overview

The architecture combines several technologies:

  • Jotai for state management
  • oRPC for type-safe API calls
  • React Query for server state management
  • React Suspense for loading states
  • Next.js for server-side rendering

Core Concepts

oRPC Setup

The application uses two oRPC clients:

  1. orpc-vanilla-client.ts - Base oRPC client used by Jotai atoms and direct calls
  2. orpc-server.ts - Server-side helpers for SSR

Query Atoms

Queries are built using utility functions that create Jotai atoms:

  1. The base oRPC client is stored in a Jotai atom (orpcAtom) that handles authentication tokens
  2. Query atoms are created using helper functions that combine oRPC, React Query, and Jotai
  3. These atoms can be used with Suspense boundaries for loading states
  4. The orpcQueryUtilsAtom provides query utilities for cache invalidation and prefetching

Authentication Flow

The system handles authentication through:

  • Server-side: Cookie-based session token in orpc-server.ts
  • Client-side: Token management via authTokenAtom and authTokenGetterAtom

Hydration Process

  1. Server-side rendering:

    • orpcSSR creates server-side helpers
    • Initial data is prefetched and serialized
    • QueryClient is populated with initial data
  2. Client-side hydration:

    • React Query rehydrates from serialized state
    • Jotai atoms initialize with prefetched data
    • Components render with cached data first

Implementation Details

Server-Side Setup

The server-side oRPC setup (orpc-server.ts) handles SSR and authentication:

export const orpcSSR = (queryClient: QueryClient) => { const token = cookies().get('__session') return createServerSideHelpers({ client: getProxyClient(token?.value), queryClient, }) }

Provider Architecture

The application uses a layered provider architecture to handle state management:

  1. Business Providers (business-providers.tsx):

    • Sets up the root providers for the application
    • Configures React Query with SSR-friendly defaults
    • Initializes Jotai store and auth state
  2. Jotai Query Provider (jotai-query-provider.tsx):

    • Creates a scoped Jotai store
    • Manages auth token and query client state
    • Enables component-level state isolation

Query Atom Creation

The makeQueryAtoms utility (make-query-atoms.ts) provides a standardized way to create query atoms:

const [queryAtom, statusAtom] = makeQueryAtoms( ['domain', 'contextQueryKey'], (get) => params, (orpc, params) => orpc.someEndpoint.queryOptions(params) )

This creates:

  • A suspense-enabled query atom
  • A status atom for tracking fetch state

oRPC Query and Mutation Options

oRPC provides .queryOptions() and .mutationOptions() methods that return React Query-compatible options objects:

// Query options - returns { queryKey, queryFn, ... } const options = orpc.inventory.getProducts.queryOptions({ limit: 10 }) // Use with useQuery or query atoms const { data } = useQuery(options) // Mutation options - returns { mutationKey, mutationFn, ... } const mutationOptions = orpc.inventory.updateProduct.mutationOptions() // Use with useMutation const mutation = useMutation(mutationOptions)

Direct oRPC Calls

For simple cases, you can call oRPC endpoints directly:

// Direct call (returns promise) const products = await orpc.inventory.getProducts({ limit: 10 }) // With the orpc client from atom const orpc = get(orpcAtom) const result = await orpc.inventory.getProducts({ limit: 10 })

Practical Usage Examples

1. Inventory Management

The inventory UI demonstrates a complex implementation:

// Atom creation export const [productsAtom, productsStatusAtom] = makeQueryAtoms( ['inventory', 'all'], (get) => get(fetchInventoryProductsParamsAtom), (orpc, params) => orpc.inventory.getProducts.queryOptions(params) ) // Component usage const InventoryProducts = () => { const products = useAtomValue(productsSwrAtom) const status = useAtomValue(productsStatusAtom) // ... render logic }

2. Mutations

Mutations are handled similarly, as shown in the edit inventory dialog:

const EditInventoryLine = () => { const { mutateAsync: editInventoryLine } = useAtomValue(editInventoryLineMutation) const handleSubmit = async () => { await editInventoryLine() // Invalidate relevant queries await queryClient.invalidateQueries({ queryKey: queries.inventory._def }) } }

Error Handling

oRPC provides utilities for type-safe error handling with isDefinedError() and safe() from @orpc/client:

Using safe() for Error Handling

The safe() utility wraps async calls and returns a tuple of [error, data]:

import { safe, isDefinedError } from '@orpc/client' const handleSubmit = async () => { const [error, data] = await safe(mutation.mutateAsync(input)) if (error) { if (isDefinedError(error)) { // Type-safe error handling for oRPC defined errors switch (error.code) { case 'ITEM_NOT_FOUND': toast.error(`Item ${error.data.itemId} not found`) break case 'INSUFFICIENT_QUANTITY': toast.error(`Only ${error.data.available} items available`) break } } else { // Generic error handling toast.error('An unexpected error occurred') } return } // Success - data is typed toast.success(`Created item ${data.id}`) }

Using isDefinedError() for Type Guards

The isDefinedError() function is a type guard that narrows the error type to oRPC’s defined errors:

import { isDefinedError } from '@orpc/client' try { await orpc.cart.addItem({ itemId: '123' }) } catch (error) { if (isDefinedError(error)) { // error.code and error.data are now typed console.log(error.code) // e.g., 'ITEM_NOT_FOUND' console.log(error.data) // e.g., { itemId: '123' } } }

Query Utilities with orpcQueryUtilsAtom

The orpcQueryUtilsAtom provides utilities for cache management and prefetching:

const queryUtils = useAtomValue(orpcQueryUtilsAtom) // Invalidate queries await queryUtils.inventory.getProducts.invalidate() // Prefetch data await queryUtils.inventory.getProducts.prefetch({ limit: 10 }) // Set query data directly queryUtils.inventory.getProducts.setData({ limit: 10 }, newData)

Best Practices for Implementation

  1. Query Organization

    • Group related queries in feature-specific files
    • Use consistent naming patterns for atoms
    • Implement proper error boundaries
  2. State Management

    • Use atoms for local UI state
    • Leverage React Query for server state
    • Implement proper loading states with Suspense
  3. Performance Optimization

    • Configure appropriate stale times
    • Implement query invalidation strategies
    • Use proper caching policies
  4. Type Safety

    • Leverage oRPC’s type inference
    • Define proper input/output types in @repo/api-dtos
    • Use TypeScript strict mode
    • Use isDefinedError() for typed error handling
  5. Error Handling

    • Use safe() wrapper for cleaner error handling
    • Handle defined errors with type-safe switches
    • Provide meaningful error messages to users

Common Patterns

  1. Query with Status
const [dataAtom, statusAtom] = makeQueryAtoms( ['domain', 'contextQueryKey'], getParams, (orpc, params) => orpc.domain.endpoint.queryOptions(params) ) // Usage const data = useAtomValue(dataAtom) const status = useAtomValue(statusAtom)
  1. Mutation with Invalidation
const mutation = useSetAtom(mutationAtom) const queryClient = useAtomValue(queryClientAtom) const handleMutate = async () => { const [error, data] = await safe(mutation(input)) if (error) { if (isDefinedError(error)) { // Handle typed error } return } await queryClient.invalidateQueries({ queryKey: queries.domain._def }) }
  1. Suspense Boundary Setup
<Suspense fallback={<LoadingComponent />}> <QueryComponent /> </Suspense>
  1. Direct oRPC Call with Error Handling
const orpc = useAtomValue(orpcAtom) const fetchData = async () => { const [error, data] = await safe(orpc.inventory.getProduct({ id })) if (isDefinedError(error)) { // Handle specific error codes } return data }

This architecture provides a robust foundation for building complex applications with server state management, while maintaining type safety and optimal performance characteristics.

Last updated on