Frontend Architecture
The folksbase frontend (apps/web) is built with Next.js 15 using the App Router. The guiding principle is simple: default to React Server Components (RSC). Only add 'use client' when you genuinely need it.
Why RSC-First?
React Server Components render on the server and send HTML to the browser — no JavaScript bundle for those components. This matters because:- Less JavaScript shipped to the client. Pages that just display data (contact lists, dashboard stats, settings) don’t need client-side React at all.
- Direct data access. Server Components can fetch data directly — no API calls from the browser, no loading spinners for initial content.
- Better performance on slow connections. The browser receives rendered HTML instead of a JavaScript bundle that needs to execute before anything appears.
When to Use What
Use RSC (the default) when:
- Fetching and displaying data
- Rendering static or server-rendered content
- The component doesn’t need interactivity
Use 'use client' when:
- You need event handlers (
onClick,onChange, etc.) - You need browser APIs (
localStorage,window, etc.) - You need SWR hooks for client-side data fetching
- You need state that changes without navigation (real-time progress bars, optimistic updates)
When you need both
Split into two components: a Server Component wrapper that fetches data, and a Client Component leaf that handles interaction. Never add'use client' to a data-fetching component.
Component Structure
Components are organized by domain, not by type:ui/ directory contains reusable atoms — components with no business logic and no data fetching. Everything else is organized by the feature it belongs to.
Compound components are used for complex UI. For example, the contacts table uses a compound pattern where the table, rows, cells, and actions are composed together. See contacts/contacts-table.tsx for the pattern.
Data Fetching Patterns
folksbase uses two data fetching strategies depending on the context:RSC Pages — Direct Fetch
Server Component pages fetch data directly. No hooks, no loading states for the initial render:Client Components — SWR Hooks
Client components that need real-time updates use SWR hooks fromhooks/use-*.ts:
['/endpoint', { ...params }]. This makes cache invalidation predictable.
Dashboard Pattern — Best of Both
The dashboard combines both approaches for instant render with live updates:- The RSC page prefetches all data (stats, contacts, imports, exports, tags)
- Passes it as
initialDatato the client component - The client component wraps children in
<SWRConfig value={{ fallback }}> - SWR uses the prefetched data immediately, then revalidates in the background
Why These Choices?
Why Next.js 15 with App Router?
The App Router’s file-based routing, nested layouts, and native RSC support make it the natural choice for a React app that prioritizes performance. The alternative (Pages Router) doesn’t support Server Components.Why SWR over React Query?
SWR is simpler and lighter. folksbase doesn’t need React Query’s advanced features (infinite queries, mutations with rollback). SWR’suseSWR hook with mutate for cache invalidation covers all the use cases here.
Why Radix Primitives?
Building accessible UI components from scratch is hard and error-prone. Radix provides unstyled, accessible primitives (dialogs, dropdowns, selects) that handle focus management, keyboard navigation, and ARIA attributes correctly. folksbase styles them with Tailwind.Performance Patterns
CSV review virtualization
The CSV import review step needs to render files with 500K+ rows in the browser. Rather than parsing the entire file into memory, the frontend uses an index-and-on-demand approach:csv-parser.tsbuilds a lightweight byte-offset index of row positions (~4 MB for 500K rows vs ~200 MB for fully parsed arrays)getRow()parses a single row on demand using the offset — only ~30 rows exist in memory at any time@tanstack/react-virtualrenders a fixed window of DOM elements regardless of file size
Retry logic for mutations
fetchApi() in apps/web/src/lib/api.ts retries non-GET requests once with a 1-second delay on transient server failures (502, 503, 504) and network TypeErrors. Client errors (4xx) are never retried. GET requests rely on SWR’s built-in revalidation instead.
Storybook
UI components are documented with Storybook 8. Stories live alongside their components as*.stories.tsx files.
- Config lives in
apps/web/.storybook/ - Next.js dependencies (
next/image,next/navigation) are mocked - Auto-deploys to Netlify when component or storybook config files change on
main