# Components How to build server and client components inside a feature folder. ## Default: async server component Server components await their own queries directly — no `useEffect`, no client-side fetching, no manual loading state. See the [Server Components docs](https://preview.nextjs.org/docs/app/getting-started/server-and-client-components) for the model. Prefer minimal, stable props: IDs, slugs, handles, parsed filters, or records the parent already fetched. Do not pass raw route `params` or `searchParams` into feature components. Pages resolve those promises and pass plain values. ```tsx // features/notifications/components/notifications-badge.tsx import { getUnreadNotificationCount } from "@/features/notifications/notifications-queries"; export async function NotificationsBadge() { const count = await getUnreadNotificationCount(); if (count === 0) return null; return {count}; } ``` The page (not this file) wraps it in `}>` — see `references/pages-suspense.md`. For parameterized routes, the page resolves `params` and the feature receives an ID: ```tsx // app/post/[id]/page.tsx }> {params.then(({ id }) => ( ))} ``` ```tsx // features/post/components/post-detail.tsx export async function PostDetail({ id }: { id: string }) { const post = await getPost(id); return
{post.body}
; } ``` ## Skeletons live in the same file Export the main component and its skeleton from the same file. Pages import both. Define the skeleton **at the end of the file**, below the real component(s) — never above. Function declarations are hoisted, so a skeleton referenced by a component earlier in the file still works when defined last. ```tsx export async function Feed({ userId }: { userId: string }) { const posts = await getFeed(userId); return ( ); } export function FeedSkeleton() { return ( ); } ``` Don't export a second skeleton whose whole job is to rename or preconfigure another skeleton: ```tsx // Wrong — alias wrapper adds an import surface but no behavior export function CompactGridSkeleton() { return ; } ``` Import the real skeleton and pass the prop inline at the `` boundary: `fallback={}`. ### Skeleton design checklist 1. Match the real component's layout: flex direction, gaps, padding, breakpoints. 2. Include all structural elements: avatar circles, action button placeholders, image squares. 3. Responsive visibility must match (`hidden sm:block` in the real component → same in the skeleton). 4. Show 2–5 placeholders for variable-length lists, not the real count. 5. Don't include skeletons for inner Suspense content — those have their own boundaries. 6. Reserve the right height. CLS comes from skeletons that are shorter than the real content. ## Group related components in one file A card and its grid live in the same file. For example, `genre-card.tsx` exports `GenrePill`, `GenreCard`, `GenreGrid`, `GenreGridSkeleton`. Variants should reuse that skeleton inline instead of exporting alias skeletons. Don't split shared UI primitives prematurely — wait until three call sites need the same shape before extracting. Two sidebar widgets that happen to look similar but render different data shapes are **not** the same component. The visuals diverge as soon as one needs an extra slot. ### Single-use sub-components stay inlined For a metadata strip inside one card, a header used only by one detail view, a list item only rendered by its list — inline them as **non-exported** functions in the same file: ```tsx export async function EventDetails({ slug }: { slug: string }) { const event = await getEventBySlug(slug); return (

{event.description}

); } function MetaStrip({ event }: { event: Event }) { ... } function Speaker({ speaker }: { speaker: string }) { ... } ``` Exports are for things other files will import. Internal structure is for readability inside one file. ## The server/client boundary `'use client'` only when you need: - Hooks (`useState`, `useReducer`, `useOptimistic`, `useTransition`, `useEffect`) - Event handlers (`onClick`, `onChange`, `onSubmit`) - Browser APIs (`window`, `localStorage`, refs to DOM) If the component needs interactive pieces, keep the server component as the parent and render client leaves: ```tsx async function PostDetail({ id }: { id: string }) { const [post, userState] = await Promise.all([ getPost(id), getPostUserState(id), ]); return (
{/* 'use client' leaf */}
); } ``` ### Server content as children of client components Composition crosses the boundary. A client component can accept server-rendered JSX as children or props: ```tsx }>
} /> ``` `ComposerForm` is `'use client'`. It doesn't know where the avatar JSX came from. The Suspense boundary streams the avatar in without the form re-rendering. ### Pass server children resolved values, not promises Prefer passing plain values (strings, IDs, resolved data) to a server child. A server component _can_ `await` a promise prop, but resolve route promises in the page instead — pass an unresolved promise down only to a _client_ component that reads it with `use()` (see below). When a parent already has the data from its own query, pass it as a prop instead of having the child refetch. ```tsx // Right — parent fetches the list, passes each item async function Feed({ userId }: { userId: string }) { const posts = await getFeed(userId); return posts.map((post) => ); } async function Post({ post }: { post: Post }) { return
{post.body}
; } ``` ```tsx // Wrong — child refetches what the parent already had async function Post({ id }: { id: string }) { const post = await getPost(id); return
{post.body}
; } ``` ## Client components that own their loading state When a client component needs server data but should manage its own loading (a sidebar badge, a popover that opens on hover), pass an **unresolved promise** from the server and resolve it with [`use()`](https://react.dev/reference/react/use) on the client. Wrap the consumer in ``. ```tsx // page: pass the unresolved promise, wrap in Suspense }> ``` ```tsx "use client"; import { use } from "react"; export function TagPicker({ itemsPromise }: { itemsPromise: Promise }) { const items = use(itemsPromise); // render interactive UI from items } ``` The opinionated bit: name promise props with a `Promise` suffix (`itemsPromise`, `userPromise`) so the contract is obvious at the call site. ### Client data libraries (SWR, TanStack Query) Follow `references/single-page-applications.md` when a feature uses a browser data cache or needs externally authored updates. It covers when to use a library, where its files live, server seeding, Cache Components coordination, hydration, and mutations. ## Mutations For client-side reactions to a server mutation (instant feedback, pending state, success/error toasts), see `references/ux-patterns.md`. To cache rendered output across requests, see `references/cache-components.md`.