Files
kolaytercih/.claude/skills/nextjs-app-architecture/references/ux-patterns.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

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 with router.push().
  • Don't wrap the whole action call in useTransition inside the dialog — with view transitions on, that animates the background UI behind the dialog. Track pending with useState / useOptimistic(false) and reserve startTransition for 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.