Using Manual Cache Invalidation

Avatar of Hemanta SundarayHemanta Sundaray

In the previous chapter, we used reactivityKeys to automatically refresh the user list after deletions, and useAtomRefresh to explicitly refresh after additions. These cover most scenarios. But sometimes you need more control. Perhaps you want to invalidate based on a condition, or trigger a refresh from a component after some user interaction that isn’t tied to a specific mutation. For these cases, you can create an invalidation Atom.

Creating an Invalidation Atom

In atoms/user.ts, add the invalidateUsersAtom:

atoms/user.ts
import { Duration, Effect } from "effect";
import { Atom, Reactivity } from "effect/unstable/reactivity";
import { atomRuntime } from "@/atom-runtime";
import { type AddUserFormValues } from "@/schema/user-schema";
import { UserService } from "@/services/user-service";
38 collapsed lines
// ============ Users Atom ============
export const usersAtom = atomRuntime
.atom(
Effect.gen(function* () {
const userService = yield* UserService;
return yield* userService.getUsers();
}),
)
.pipe(Atom.setIdleTTL(Duration.hours(1)), Atom.withReactivity(["users"]));
// ============ User Atom ============
export const userAtom = Atom.family((id: string) =>
atomRuntime
.atom(
Effect.gen(function* () {
const userService = yield* UserService;
return yield* userService.getUser(id);
}),
)
.pipe(Atom.setIdleTTL(Duration.hours(1))),
);
// ============ Delete User Atom ============
export const deleteUserAtom = atomRuntime.fn(
Effect.fnUntraced(function* (userId: string) {
const userService = yield* UserService;
yield* userService.deleteUser(userId);
}),
{ reactivityKeys: ["users"] },
);
// ============ Add User Atom ============
export const addUserAtom = atomRuntime.fn<AddUserFormValues>()(
Effect.fnUntraced(function* (user) {
const userService = yield* UserService;
return yield* userService.addUser(user);
}),
);
// ============ Invalidate Users Atom ============
export const invalidateUsersAtom = atomRuntime.fn()(
Effect.fnUntraced(function* () {
yield* Reactivity.invalidate(["users"]);
}),
);

This creates a function Atom that, when called, invalidates all Atoms tagged with the "users" key. Keep in mind that, like reactivityKeys, this uses the reactivity system and only reaches Atoms with active subscribers.

Using Manual Invalidation

To see how this works, let’s temporarily use manual invalidation for the user deletion flow.

First, comment out the reactivityKeys option on deleteUserAtom in atoms/user.ts:

atoms/user.ts
import { Duration, Effect } from "effect";
import { Atom, Reactivity } from "effect/unstable/reactivity";
import { atomRuntime } from "@/atom-runtime";
import { type AddUserFormValues } from "@/schema/user-schema";
import { UserService } from "@/services/user-service";
21 collapsed lines
// ============ Users Atom ============
export const usersAtom = atomRuntime
.atom(
Effect.gen(function* () {
const userService = yield* UserService;
return yield* userService.getUsers();
}),
)
.pipe(Atom.setIdleTTL(Duration.hours(1)), Atom.withReactivity(["users"]));
// ============ User Atom ============
export const userAtom = Atom.family((id: string) =>
atomRuntime
.atom(
Effect.gen(function* () {
const userService = yield* UserService;
return yield* userService.getUser(id);
}),
)
.pipe(Atom.setIdleTTL(Duration.hours(1))),
);
// ============ Delete User Atom ============
export const deleteUserAtom = atomRuntime.fn(
Effect.fnUntraced(function* (userId: string) {
const userService = yield* UserService;
yield* userService.deleteUser(userId);
}),
// { reactivityKeys: ["users"] },
);
14 collapsed lines
// ============ Add User Atom ============
export const addUserAtom = atomRuntime.fn<AddUserFormValues>()(
Effect.fnUntraced(function* (user) {
const userService = yield* UserService;
return yield* userService.addUser(user);
}),
);
// ============ Invalidate Users Atom ============
export const invalidateUsersAtom = atomRuntime.fn()(
Effect.fnUntraced(function* () {
yield* Reactivity.invalidate(["users"]);
}),
);

Now in components/user-delete-dialog.tsx, import and call the invalidation Atom after a successful deletion:

components/user-delete-dialog.tsx
"use client";
import { useState } from "react";
import { useAtomSet } from "@effect/atom-react";
import { Cause, Exit, Option } from "effect";
import { toast } from "sonner";
import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { deleteUserAtom, invalidateUsersAtom } from "@/atoms/user";
import type { User } from "@/schema/user-schema";
interface UserDeleteDialogProps {
user: User;
trigger: React.ReactNode;
}
export function UserDeleteDialog({ user, trigger }: UserDeleteDialogProps) {
const [open, setOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);
const deleteUser = useAtomSet(deleteUserAtom, { mode: "promiseExit" });
const invalidateUsers = useAtomSet(invalidateUsersAtom);
function handleOpenChange(isOpen: boolean) {
setOpen(isOpen);
if (!isOpen) {
setError(null);
}
}
async function handleDelete() {
setIsDeleting(true);
setError(null);
const exit = await deleteUser(user.id);
setIsDeleting(false);
if (Exit.isSuccess(exit)) {
invalidateUsers(undefined);
setOpen(false);
toast.success("User deleted successfully", {
description: `${user.firstName} ${user.lastName} has been deleted.`,
});
} else {
const failureOption = Cause.findErrorOption(exit.cause);
const errorMessage = Option.isSome(failureOption)
? failureOption.value.message
: "An unexpected error occurred";
setError(errorMessage);
}
}
return (
36 collapsed lines
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className="sm:max-w-110">
<DialogHeader>
<DialogTitle>
Delete {user.firstName} {user.lastName}?
</DialogTitle>
<DialogDescription>
Are you sure? This action cannot be undone.
</DialogDescription>
</DialogHeader>
{error && (
<div className="flex items-center gap-2 border border-red-200 bg-red-50 p-3 text-sm text-red-600">
<Icons.alert className="size-4 shrink-0" />
<span>{error}</span>
</div>
)}
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" disabled={isDeleting}>
Cancel
</Button>
</DialogClose>
<Button
variant="destructive"
onClick={handleDelete}
disabled={isDeleting}
>
{isDeleting && <Icons.spinner className="size-4 animate-spin" />}
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

We call invalidateUsers(undefined) after confirming the deletion succeeded. useAtomSet always returns a setter function that expects one argument, which is the input to the function Atom. With deleteUserAtom, that argument is the user ID. With addUserAtom, it’s the form data. But invalidateUsersAtom doesn’t need any input; it just triggers an invalidation. Since the setter function still requires an argument, we pass undefined to satisfy it.

Now, delete a user. The user list should update automatically, just as it did before with reactivityKeys. The difference is that this time, the invalidation is triggered manually by calling invalidateUsersAtom after the deletion succeeds, rather than being handled automatically by the reactivity system.

Automatic vs Manual Invalidation

For same-page mutations, automatic invalidation via reactivityKeys is the right fit. For cross-page mutations, useAtomRefresh handles it directly. Manual invalidation via invalidateUsersAtom is useful when you need to refresh data based on events that aren’t tied to a mutation Atom. For example, a ‘Refresh’ button, or after a complex multi-step workflow where you only want to invalidate at the end.

Since automatic invalidation is the right choice here, let’s revert our changes. In atoms/user.ts, uncomment the reactivityKeys option on deleteUserAtom. Then in components/user-delete-dialog.tsx, remove the invalidateUsersAtom import and the invalidateUsers() call. The reactivityKeys on deleteUserAtom already handles the invalidation.

Keep the invalidateUsersAtom definition in atoms/user.ts though. It’s a useful reference for when you need manual control in the future.

Last updated on March 11, 2026

Sign in to save progress

Stay in the loop

Get notified when new Effect Atom related content is published.