Rerendering After Server Actions

Show a server action's changes without leaving the page, so useOptimistic does not snap back.


An Action Renders Nothing by Default

When a server action settles, Waku returns its value and leaves the page as it was last rendered. After a mutation, that means the page shows stale data. It also undoes useOptimistic: React drops the optimistic value when the action settles, expecting the page to show the change by then, so the update appears and then disappears.

Rerender the Page the Action Came From

Call requestRefresh() from waku/router/server. Waku renders the route the action was called from into the action's response, so the page and the optimistic value settle on the same data. The call does not terminate execution or change the action's return value.

// src/pages/cart.tsx
import { requestRefresh } from 'waku/router/server';
import { AddToCart } from '../components/add-to-cart';
import { addItem, getCart } from '../lib/cart';

export default async function CartPage() {
  const cart = await getCart();

  return (
    <AddToCart
      count={cart.items.length}
      addItem={async (formData: FormData) => {
        'use server';

        await addItem(String(formData.get('id')));
        requestRefresh();
      }}
    />
  );
}

export const getConfig = async () => {
  return {
    render: 'dynamic',
  } as const;
};
// src/components/add-to-cart.tsx
'use client';

import { useOptimistic } from 'react';

export const AddToCart = ({
  count,
  addItem,
}: {
  count: number;
  addItem: (formData: FormData) => Promise<void>;
}) => {
  const [optimisticCount, setOptimisticCount] = useOptimistic(count);

  return (
    <form
      action={async (formData) => {
        setOptimisticCount(optimisticCount + 1);
        await addItem(formData);
      }}
    >
      <p>{optimisticCount} items</p>
      <input type="hidden" name="id" value="42" />
      <button>Add to cart</button>
    </form>
  );
};

The refresh runs after the action succeeds, so it reads the completed mutation. Repeated calls to requestRefresh() coalesce into one refresh. Finish all mutations, then request a refresh to show their results. Refetch independent slices from the client when several views need updating. The page has to render dynamically for the rerender to read the new data. If the action requests a redirect or not-found view, Waku skips its scheduled rerenders and returns the action's value with that intent.

If the user moves to another page or a navigation is pending when the action returns, Waku drops the origin rerender. This includes a reload of the same route. The action still returns its value.

Overlapping actions that request refreshes can also have a refresh dropped while another is waiting to commit. Do not rely on every concurrent action's result appearing on screen.

Deprecated Rerender API

unstable_rerenderRoute() is deprecated and will be removed in a future release. Replace it with requestRefresh(). The named-route overload, unstable_rerenderRoute(pathname, query), is also deprecated. Both forms retain their existing behavior until removal.

Use requestRedirect() to navigate after a mutation:

requestRedirect('/cart?tab=saved');

See Redirects and Not Found.

Forms Without JavaScript

Without JavaScript, the form posts to the page and Waku renders that page after the action runs, so the document already includes the change. Calling requestRefresh() there also works; it renders the same page.

Avoid Rerendering From an Effect

Do not call requestRefresh() in an action that a client component calls from an effect with the action in its dependencies:

useEffect(() => {
  void track();
}, [track]);

Every rerender gives the client a new track function, which runs the effect again and calls the action again, in a loop. This is also why Waku does not rerender after every action by default.

Router Requirement

requestRefresh() needs a router on the client that tells the server which route the action came from. Waku's router does, and a router built on waku/router/client-core does by installing an unstable_createServerFunctionRouting() controller's enhancer on Root and connecting it with useServerFunctionRouting_UNSTABLE(controller, callbacks). In an action that did not come through such a router, requestRefresh() throws.

designed bycandycode alternative graphic design web development agency San Diego