> ## Documentation Index
> Fetch the complete documentation index at: https://docs.folksbase.joselito.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Frontend Architecture

> RSC-first approach, component structure, and data fetching patterns in the Next.js app.

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:

1. **Less JavaScript shipped to the client.** Pages that just display data (contact lists, dashboard stats, settings) don't need client-side React at all.
2. **Direct data access.** Server Components can fetch data directly — no API calls from the browser, no loading spinners for initial content.
3. **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.

```
// ✅ Server Component fetches, Client Component interacts
// contacts/page.tsx (Server Component — no directive needed)
export default async function ContactsPage() {
  const data = await fetchContacts();
  return <ContactsPageContent initialData={data} />;
}

// contacts-page-content.tsx (Client Component)
'use client'
export function ContactsPageContent({ initialData }) {
  const { data } = useContacts({ initialData });
  return <ContactsTable contacts={data} />;
}
```

## Component Structure

Components are organized by domain, not by type:

```
components/
├── ui/           # Atoms — buttons, badges, inputs, dialogs
│                 # No business logic, no data fetching
├── contacts/     # Contact list, table, drawer, search
├── imports/      # Upload drop zone, progress tracker, column mapper
├── exports/      # Export dialog, progress, download
├── tags/         # Tag input, tag filter, tag management
├── settings/     # Workspace settings, API key management
├── dashboard/    # Stat cards, growth chart, recent activity
└── layout/       # App shell: sidebar, header, command palette
```

The `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:

```typescript theme={"dark"}
// app/(dashboard)/contacts/page.tsx
export default async function ContactsPage() {
  const contacts = await apiServer.contacts.list();
  const tags = await apiServer.tags.list();
  return <ContactsPageContent initialContacts={contacts} initialTags={tags} />;
}
```

### Client Components — SWR Hooks

Client components that need real-time updates use SWR hooks from `hooks/use-*.ts`:

```typescript theme={"dark"}
// hooks/use-contacts.ts
export function useContacts(params) {
  return useSWR(['/contacts', { cursor, search, tag }], fetcher);
}
```

SWR keys follow a consistent pattern: `['/endpoint', { ...params }]`. This makes cache invalidation predictable.

### Dashboard Pattern — Best of Both

The dashboard combines both approaches for instant render with live updates:

1. The RSC page prefetches all data (stats, contacts, imports, exports, tags)
2. Passes it as `initialData` to the client component
3. The client component wraps children in `<SWRConfig value={{ fallback }}>`
4. SWR uses the prefetched data immediately, then revalidates in the background

This means the dashboard loads instantly (server-rendered HTML) and stays fresh (SWR revalidation).

## 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's `useSWR` 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.ts` builds 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-virtual` renders a fixed window of DOM elements regardless of file size

This pattern keeps memory usage proportional to the raw file size rather than exploding with per-row object overhead. See the [CSV Import review step](/features/csv-import#phase-25-review-step--large-file-handling) for the full breakdown.

### 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 `TypeError`s. 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`

## What's Next?

<CardGroup cols={2}>
  <Card title="Backend Architecture" icon="server" href="/architecture/backend">
    The layered API pattern and middleware stack.
  </Card>

  <Card title="Database Schema" icon="database" href="/architecture/database">
    Tables, relationships, and the multi-tenancy model.
  </Card>
</CardGroup>
