Files
kolaytercih/.claude/skills/nextjs-app-architecture/references/components.md
bilalgursen 820eea7eca refactor: ortak tipler @/types/yokatlas + features/rapor/types'a çıkarıldı, server-only korumaları eklendi (Faz 1)
- Program/PUAN_TURLERI/DilimKey/RankResults/UniturGrubu/ProgramNetSatiri/
  SihirbazFacetleri artık src/types/yokatlas.ts'te; lib/db.ts yalnızca sorgu
  fonksiyonları barındırıyor
- RaporSonuc/RaporKapsam/RaporParams/MaskeliRapor src/features/rapor/types/
  rapor.ts'e taşındı; ölü kalan TercihSchema/RaporSchema silindi
- 11 lib dosyasına import "server-only" eklendi; scripts'in düz node ile
  import ettiği dosyalara (db, appdb, rapor-havuzu, tadimlik-havuzu, ai/client,
  ai/cagri, credits) bilinçli eklenmedi
- Davranış değişikliği yok; pnpm build yeşil

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 01:06:06 +03:00

7.8 KiB
Raw Blame History

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 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.

// 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 <span aria-label={`${count} unread`}>{count}</span>;
}

The page (not this file) wraps it in <Suspense fallback={<NotificationsBadgeSkeleton />}> — see references/pages-suspense.md.

For parameterized routes, the page resolves params and the feature receives an ID:

// app/post/[id]/page.tsx
<Suspense fallback={<PostDetailSkeleton />}>
  {params.then(({ id }) => (
    <PostDetail id={id} />
  ))}
</Suspense>
// features/post/components/post-detail.tsx
export async function PostDetail({ id }: { id: string }) {
  const post = await getPost(id);
  return <article>{post.body}</article>;
}

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.

export async function Feed({ userId }: { userId: string }) {
  const posts = await getFeed(userId);
  return (
    <ul>
      {posts.map((p) => (
        <Post key={p.id} post={p} />
      ))}
    </ul>
  );
}

export function FeedSkeleton() {
  return (
    <ul>
      {Array.from({ length: 3 }).map((_, i) => (
        <li key={i}>
          <Skeleton className="h-24" />
        </li>
      ))}
    </ul>
  );
}

Don't export a second skeleton whose whole job is to rename or preconfigure another skeleton:

// Wrong — alias wrapper adds an import surface but no behavior
export function CompactGridSkeleton() {
  return <GridSkeleton dense />;
}

Import the real skeleton and pass the prop inline at the <Suspense> boundary: fallback={<GridSkeleton dense />}.

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 25 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.

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:

export async function EventDetails({ slug }: { slug: string }) {
  const event = await getEventBySlug(slug);
  return (
    <article>
      <MetaStrip event={event} />
      <Speaker speaker={event.speaker} />
      <p>{event.description}</p>
    </article>
  );
}

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:

async function PostDetail({ id }: { id: string }) {
  const [post, userState] = await Promise.all([
    getPost(id),
    getPostUserState(id),
  ]);
  return (
    <article>
      <PostBody body={post.body} />
      <PostActions userState={userState} /> {/* 'use client' leaf */}
    </article>
  );
}

Server content as children of client components

Composition crosses the boundary. A client component can accept server-rendered JSX as children or props:

<ComposerForm
  avatar={
    <Suspense fallback={<AvatarSkeleton />}>
      <CurrentUserAvatar />
    </Suspense>
  }
/>

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.

// Right — parent fetches the list, passes each item
async function Feed({ userId }: { userId: string }) {
  const posts = await getFeed(userId);
  return posts.map((post) => <Post key={post.id} post={post} />);
}

async function Post({ post }: { post: Post }) {
  return <article>{post.body}</article>;
}
// Wrong — child refetches what the parent already had
async function Post({ id }: { id: string }) {
  const post = await getPost(id);
  return <article>{post.body}</article>;
}

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() on the client. Wrap the consumer in <Suspense>.

// page: pass the unresolved promise, wrap in Suspense
<Suspense fallback={<TagListSkeleton />}>
  <TagPicker itemsPromise={getTags()} />
</Suspense>
"use client";
import { use } from "react";

export function TagPicker({ itemsPromise }: { itemsPromise: Promise<Tag[]> }) {
  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.