Skip to content

Frontend (React)

How the React app is structured.

Module structure

Frontend modules mirror the backend structure:

src/modules/projects/
├── ProjectList.tsx
├── ProjectForm.tsx
├── ProjectDetail.tsx
├── api.ts             # API client functions
├── hooks.ts           # Custom hooks
└── types.ts           # Module-specific types

State management

  • Server state: React Query (@tanstack/react-query) — handles caching, refetching, mutations
  • UI state: Zustand for cross-component state, useState for component-local
  • Forms: React Hook Form + Zod for validation
// Server state example
const { data: projects, isLoading } = useQuery({
  queryKey: ["projects"],
  queryFn: () => api.projects.list(),
})

// Mutation
const createProject = useMutation({
  mutationFn: api.projects.create,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ["projects"] }),
})

Routing

React Router v6 with file-based-ish structure via routes.tsx:

const routes = [
  { path: "/", element: <Dashboard /> },
  { path: "/projects", element: <ProjectList /> },
  { path: "/projects/:id", element: <ProjectDetail /> },
  // ...
]

Forms

Every form follows this pattern:

const schema = z.object({
  name: z.string().min(1).max(255),
  reference: z.string().min(1).max(100),
})

type FormValues = z.infer<typeof schema>

export function ProjectForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({
    resolver: zodResolver(schema),
  })

  const onSubmit = (data: FormValues) => { /* ... */ }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <TextField label="Name" {...register("name")} error={errors.name?.message} />
      ...
    </form>
  )
}

API client

Generated from OpenAPI spec. Don't hand-write API calls:

npm run generate-types

Offline-capable components

For modules that work offline, use PowerSync hooks instead of React Query:

const { data: projects } = usePowerSync(
  "SELECT * FROM projects WHERE deleted_at IS NULL"
)

See Offline Sync for which modules sync.

Styling

Tailwind CSS via shadcn/ui components. Don't write raw CSS unless absolutely necessary.

See also