- 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>
5.4 KiB
UX patterns
Interaction decisions on top of the architecture: which feedback mechanism to reach for, and the boundary edge-cases that trip agents up. Hook mechanics live in the React / Next docs — linked, not restated. The deeper end-to-end picture is the Building interactive apps guide.
Choose the feedback mechanism
| Situation | Reach for | Key rule |
|---|---|---|
| Mutation unlikely to fail (favorite, vote, follow) | useOptimistic |
Update immediately, roll back on throw. Set it inside a transition; inside <form action> React opens the transition for you. Use a reducer for counters. |
| No optimistic fit (filters, sort, navigation) | useTransition + data-pending |
Put data-pending on the pending node; let ancestors react with CSS (has-data-pending: for a direct parent, group-has-data-pending: further up) so it bubbles without prop drilling. |
| Form field validation ("fix this field") | useActionState |
Action returns { error }; render inline with aria-invalid + role="alert". |
| Submit disable + spinner | useFormStatus |
Call it from a child of <form>, not the form component itself. |
| One-shot result with no visible change | toast | See below. |
useOptimistic(false) also works as a transition-scoped pending flag that resets automatically when the transition settles — handy when you don't need the data-pending bubbling.
Toasts
- Toast only on error when an optimistic UI already shows the result — a success toast next to an optimistic checkmark/removal is double feedback, which is noise.
- Toast on success only for non-visible side effects (email sent, link copied, file uploaded).
- Don't toast for routine navigation — the page change is the feedback.
- Don't toast inside a server action. Toasts are client-side; return a result and toast at the call site.
View transitions: portaled / floating UI
Portaled elements (toasts, dialogs, popovers, dropdowns, tooltips) flicker during route transitions unless excluded. Apply viewTransitionName: 'none' to the portal root. When the portal also needs stacking control (z-index) or has translucent layers (backdrop-blur), give it a named transition and neutralize it in CSS instead — ::view-transition-group(name) { animation: none; z-index: … } can do things 'none' can't. See the React View Transitions skill.
Destructive actions (delete / leave / unsubscribe)
Gate behind a confirmation dialog, and mind two edge cases:
- Don't
redirect()inside the action. It throws, which stops the client from toasting or closing the dialog. Return{ ok: true }and navigate withrouter.push(). - Don't wrap the whole action call in
useTransitioninside the dialog — with view transitions on, that animates the background UI behind the dialog. Track pending withuseState/useOptimistic(false)and reservestartTransitionfor the post-success navigation only.
The action-prop pattern
A reusable design component (<ToggleGroup>, <BottomNav>, <SubmitButton>) can take an action-style prop and own the async coordination (optimistic update, pending, dimming) so consumers pass a plain callback. Convention: an action / *Action prop signals "triggers a mutation this component coordinates," versus a plain onChange / onClick — renaming between them is a contract change. Not every such prop is transition-wrapped: a destructive confirmAction is awaited without a transition (see above). Transition-wrapping is the default for optimistic/navigation actions, not a rule tied to the name.
URL-based pagination
Drive the page number through searchParams, render each page as its own <Suspense> boundary so pages stream independently, and add "load more" with <Link scroll={false}>. See linking and navigating.
import { Suspense } from 'react';
export function Feed({ page = 1 }: { page?: number }) {
return (
<ul>
{Array.from({ length: page }).map((_, i) => {
const p = i + 1;
return p === 1 ? (
<FeedPage key={p} page={p} />
) : (
<Suspense key={p} fallback={<FeedPageSkeleton />}>
<FeedPage page={p} />
</Suspense>
);
})}
</ul>
);
}
The first page can render in the parent boundary; later pages get their own fallbacks so "load more" streams only the newly requested page. If the URL update should preserve scroll, use <Link scroll={false}>.
Global client state
For truly global client state (audio player, cart, system-reactive theme), wrap a context provider at the root; the provider is 'use client' but children stays server-rendered, and only leaf components read the context. Don't push server data into it — server data stays in queries; client state is for ephemeral UI (open menus, playback position, optimistic drafts). See the live-data decision in references/components.md.