Parent passes async function, child owns useTransition, design/SubmitButton pattern.
Design components can accept action props and handle async coordination internally—keeping parent components simple.
From components/design/TabList.tsx:
'use client';
import { useOptimistic, useTransition } from 'react';
type TabListProps = {
tabs: Tab[];
activeTab: string;
changeAction?: (value: string) => void | Promise<void>;
};
export function TabList({ tabs, activeTab, changeAction }: TabListProps) {
const [optimisticTab, setOptimisticTab] = useOptimistic(activeTab);
const [isPending, startTransition] = useTransition();
function tabChangeAction(value: string) {
startTransition(async () => {
setOptimisticTab(value);
await changeAction?.(value);
});
}
return (
<Tabs value={optimisticTab}>
<TabsList>
{tabs.map(tab => (
<TabsTrigger key={tab.value} value={tab.value} onClick={() => tabChangeAction(tab.value)}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{isPending && <Loader2 className="animate-spin" />}
</Tabs>
);
}The component handles:
useOptimisticThe parent just passes the action:
export function PostTabs() {
const router = useRouter();
function tabAction(value: string) {
router.push(`/dashboard?filter=${value
Use descriptive suffixes: changeAction, submitAction, deleteAction. This signals that the prop triggers an async operation with built-in UX handling.