How we use Inertia v3 optimistic updates in There There original

by Freek Van der Herten – 4 minute read

A few months ago we started building There There, a helpdesk we're making at Spatie. The premise is simple. After two decades of running customer support for our open source work and our SaaS apps, we wanted the tool we always wished existed.

One thing we care about in particular is using AI to help humans craft better responses, not to replace them. The agent stays in charge of the conversation. The model just helps them reply faster and a little sharper. There There is in private beta right now, and you can apply for early access at there-there.app.

We're building There There with Laravel and Inertia, and we lean heavily on the latest features Inertia v3 brings. In this post I'd like to give one example: optimistic updates.

Using optimistic updates

A support agent toggles a lot of small things in the course of a day. Adding a tag to a ticket, granting a teammate access to a channel, flipping a workflow on or off. Each of those is a single click that triggers a server round-trip.

If the UI waits for that round-trip before showing the change, the app feels sluggish. Not broken, just slow. And in a tool you live in all day, that compounds.

Inertia v3 ships with a first-class optimistic updates API that handles this nicely. Instead of waiting on a response, we immediately update the UI to reflect the change and let Inertia roll it back if the request fails.

Here's how it looks on our member detail page. An admin can toggle whether a teammate belongs to a team. The list of teams comes in as a prop, and we render a switch next to each one.

{teams.map((team) => (
    <div key={team.id}>
        <span>{team.name}</span>
        <Switch
            checked={team.is_member}
            onCheckedChange={() => handleToggleTeam(team)}
        />
    </div>
))}

When the switch is flipped, handleToggleTeam runs.

function handleToggleTeam(team) {
    const optimistic = router.optimistic((props) => ({
        teams: props.teams.map((t) =>
            t.id === team.id ? { ...t, is_member: !t.is_member } : t,
        ),
    }));

    if (team.is_member) {
        optimistic.delete(removeTeamMember.url({
            team: team.ulid,
            user: member.ulid,
        }), { preserveScroll: true });

        return;
    }

    optimistic.post(addTeamMember.url({
        team: team.ulid,
        user: member.ulid,
    }), {}, { preserveScroll: true });
}

The function we pass to router.optimistic receives the current props and returns the keys we want to patch. Inertia applies that patch immediately, so the toggle in the UI flips before the request leaves the browser. When the response comes back, Inertia merges the real server props on top.

I like the chained style here because the same optimistic patch can lead to either a post or a delete. We capture the optimistic builder once and pick the verb after.

Here it is in action. The toggle flips immediately, and the request happens in the background.

For simpler cases there's a second style. You pass optimistic straight as an option to the verb. Here's our workflow toggle:

function handleToggle(workflow) {
    router.patch(workflowToggle.url(workflow.ulid), {}, {
        optimistic: (props) => ({
            workflows: props.workflows.map((w) =>
                w.id === workflow.id ? { ...w, is_enabled: !w.is_enabled } : w,
            ),
        }),
    });
}

Same idea. You describe the patched shape, hand it to Inertia, and let it deal with the rest.

If the server returns a non-2xx response, Inertia reverts the optimistic change automatically. There's no manual restore code to write. Validation errors land where you'd expect, and only the keys you touched in the callback are snapshotted, so unrelated state stays untouched. Concurrent requests each carry their own snapshot, which means a slow request won't undo a faster one that already returned.

In closing

Optimistic updates used to mean keeping a parallel copy of state, reverting it on rejection, and reasoning carefully about race conditions. With Inertia v3, you describe the next state and that's it. The whole interaction is about ten lines.

You can read the official documentation in the Inertia v3 optimistic updates guide. And if you'd like to try There There yourself, we're in private beta right now and you can apply for early access at there-there.app.

Join 9,500+ smart developers

Get my monthly newsletter with what I learn from running Spatie, building Oh Dear, and maintaining 300+ open source packages. Practical takes on Laravel, PHP, and AI that you can actually use.

No spam. Unsubscribe anytime. You can also follow me on X.

Found something interesting to share? Submit a link to the community section.