Once you have one good Action, you'll want to wrap it. Log every action. Add a confirmation step. Run a follow-up on success. Three patterns cover most of it.
Pattern 1 — wrap an action for cross-cutting concerns
You want every save action to log to your analytics service. Write a higher-order action that wraps any action and adds the side effect:
function withLogging<S>(name: string, action: (s: S, fd: FormData) => Promise<S>) {
return async (s: S, fd: FormData): Promise<S> => {
logEvent(`action.start.${name}`);
try {
const result = await action(s, fd);
logEvent(`action.success.${name}`);
return result;
} catch (e) {
logEvent(`action.failure.${name}`);
throw e;
}
};
}
const loggedSaveDraft = withLogging('saveDraft', saveDraft);
Pattern 2 — chain actions in a single submit
Sometimes one submit triggers two actions (save draft, then send email). Compose inside your action function:
async function saveAndNotify(_prev, fd) {
const draft = await saveDraft(fd);
await sendNotification(draft.id);
return draft;
}
This stays one Action from React's perspective — the pending state spans the whole chain.
Pattern 3 — extract reusable action logic into a hook
If the same action wiring (validation, logging, optimistic update) repeats across components, wrap it in a custom hook. The hook exposes the action and any related state.
Actions are functions; functions compose. No special framework required. If you'd refactor a regular async function for clarity, you'd refactor an Action the same way. Logging, retries, rate limiting, optimistic-update setup — all just function composition.