Skip to main content

Lifecycle Hooks (before/after)

Description: Asynchronous hooks that can be executed before and after the mutation process. These are defined within the options parameter of the useMutation hook.

  • How To: Provide before and after functions in the options object. Both hooks receive DataFunctionParams (including the cache key and any response data) but do not have access to the request instance (which is only available in useFetch).
    // Correct: Using before/after hooks for side effects in mutation
    const { mutate } = useMutation({
    before: async (params) => {
    // Logic to execute before mutation starts
    console.log('Starting mutation for key:', params.mutation.getKey());
    setLocalLoading(true);
    },
    after: async (params) => {
    // Logic to execute after mutation completes
    setLocalLoading(false);
    if (params.response) {
    trackAnalytics('mutation_success', {
    key: params.mutation.getKey(),
    data: params.response
    });
    }
    if (params.error) {
    console.error('Mutation failed:', params.error);
    }
    }
    });

    const handleAction = () => {
    mutate(() => api.updateUser(data));
    };
  • Best Practice: Use lifecycle hooks for surgical side effects like logging, analytics, or triggering notifications that are directly tied to the mutation. This keeps your component logic clean and ensures the side effects are executed in sync with the mutation lifecycle, regardless of whether the component unmounts.

Deep Dive: How Lifecycle Hooks work step by step

Example:

function FeedbackButton() {
const { mutate } = useMutation({
before: async () => { console.log('1. Before'); },
after: async () => { console.log('3. After'); }
});

return <button onClick={() => mutate(async () => {
console.log('2. Mutating');
return { success: true };
})}>Submit</button>;
}

What happens step by step when the button is clicked:

  1. mutate() is calleduseMutation begins its cycle.
  2. before hook is triggered → Logs '1. Before'. If it's async, Archibald waits for it to complete.
  3. The mutation action begins → Logs '2. Mutating'. This is the core async logic (e.g., API call).
  4. Mutation completes → Result is stored in the cache, so after can already read the response. The mutation stays in its loading state.
  5. after hook is triggered → Logs '3. After'. It receives the final response or error.
  6. Components are notifiedisLoading is set to false, and components re-render.

Key insight: Archibald ensures that before and after hooks are executed in the correct order relative to the mutation itself. This makes them more reliable for side effects than using useEffect, which might fire multiple times or too late during re-renders.

The mutation stays loading until after resolves

isLoading (and its alias isPending) only switches back to false once an async after hook has resolved — the mutation is not considered finished while its follow-up work is still running.

This matters when after refetches the data the mutation changed, which is the common case:

const { data: cart, isLoading: isCartLoading, refetch } = useFetch('cart', () => api.getCart());
const { mutate, isLoading: isUpdating } = useMutation({
// The mutation keeps reporting `isLoading` until this refetch has finished.
after: async () => refetch()
});

// Stays `true` for the whole interaction, instead of flickering between the two phases.
const isBusy = isUpdating || isCartLoading;

Without that guarantee the two loading states would not overlap: the mutation would report finished before the refetch reported started, and a spinner bound to isUpdating || isCartLoading would visibly toggle off and on again.

If an after hook throws, the mutation still leaves its loading state — a failing side effect cannot strand the UI in a pending state.