← Back
Edit Post
Title
Description
Action props for reusable components, encapsulate transitions and optimistic state.
Content
Markdown supported
# The Action Prop Pattern Design components should handle their own in-between states internally—optimistic updates, pending indicators, transitions—rather than pushing that complexity to every consumer. The pattern: pass the **action** to perform, let the component handle the coordination. This creates reusable components that work consistently across your app. Parents don't need to know about transitions or optimistic state—they just pass what should happen. ## The TabList Example A tablist that handles its own optimistic updates and transitions: ```tsx 'use client'; import { useOptimistic, useTransition } from 'react'; type Props = { tabs: { value: string; label: string }[]; activeTab: string; changeAction?: (value: string) => void | Promise<void>; }; export function TabList({ tabs, activeTab, changeAction }: Props) { const [optimisticTab, setOptimisticTab] = useOptimistic(activeTab); const [isPending, startTransition] = useTransition(); function handleTabChange(value: string) { startTransition(async () => { setOptimisticTab(value); await changeAction?.(value); }); } return ( <Tabs value={optimisticTab} onValueChange={handleTabChange}> <TabsList> {tabs.map(tab => ( <TabsTrigger key={tab.value} value={tab.value}> {tab.label} </TabsTrigger> ))} </TabsList> {isPending && <Spinner />} </Tabs> ); } ``` The component owns: - Optimistic tab switching (instant visual feedback) - Transition wrapping (keeps content visible during fetch) - Pending indicator (spinner next to tabs) ## Simple Parent Code Parents become simple—just pass the action: ```tsx 'use client'; import { useRouter } from 'next/navigation'; export function PostTabs({ currentFilter, currentSort }) { const router = useRouter(); return ( <TabList tabs={[ { value: 'all', label: 'All' }, { value: 'published', label: 'Published' }, { value: 'drafts', label: 'Drafts' }, ]} activeTab={currentFilter} changeAction={value => router.push(`/dashboard?filter=${value}&sort=${currentSort}`)} /> ); } ``` The parent doesn't import `useTransition`, doesn't manage optimistic state, doesn't show a spinner—that's all encapsulated. ## Naming Convention Suffix action props with "Action" to distinguish them from regular callbacks: - `changeAction` — for state changes - `submitAction` — for form submissions - `deleteAction` — for deletions This signals that the function may be async and will be wrapped in a transition. ## The Principle Design components own their in-between states. Parents pass what should happen; components handle how it looks while happening.
Published
Save Changes
Cancel