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,
useStatefor 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:
Offline-capable components¶
For modules that work offline, use PowerSync hooks instead of React Query:
See Offline Sync for which modules sync.
Styling¶
Tailwind CSS via shadcn/ui components. Don't write raw CSS unless absolutely necessary.