- 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>
215 lines
7.8 KiB
Markdown
215 lines
7.8 KiB
Markdown
# 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 <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:
|
||
|
||
```tsx
|
||
// app/post/[id]/page.tsx
|
||
<Suspense fallback={<PostDetailSkeleton />}>
|
||
{params.then(({ id }) => (
|
||
<PostDetail id={id} />
|
||
))}
|
||
</Suspense>
|
||
```
|
||
|
||
```tsx
|
||
// 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.
|
||
|
||
```tsx
|
||
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:
|
||
|
||
```tsx
|
||
// 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 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 (
|
||
<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:
|
||
|
||
```tsx
|
||
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:
|
||
|
||
```tsx
|
||
<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.
|
||
|
||
```tsx
|
||
// 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>;
|
||
}
|
||
```
|
||
|
||
```tsx
|
||
// 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()`](https://react.dev/reference/react/use) on the client. Wrap the consumer in `<Suspense>`.
|
||
|
||
```tsx
|
||
// page: pass the unresolved promise, wrap in Suspense
|
||
<Suspense fallback={<TagListSkeleton />}>
|
||
<TagPicker itemsPromise={getTags()} />
|
||
</Suspense>
|
||
```
|
||
|
||
```tsx
|
||
"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`.
|