Users often switch between browser tabs while working. When they return to your app, they might be looking at stale data. Effect Atom provides Atom.refreshOnWindowFocus to handle this. When the browser window regains focus, the Atom automatically refetches its data.
Note that our app doesn’t strictly need this feature. The reactivity system already keeps data fresh after mutations. But window focus refresh is useful in apps where data can change externally (for example, other users editing shared data), and it’s worth knowing how to set it up.
Our app has two Atoms that fetch data: usersAtom (used on the home page) and userAtom (used on the user detail page). Let’s wrap both with Atom.refreshOnWindowFocus.
Wrapping usersAtom
Update atoms/user.ts as shown below:
import { Atom } from "@effect-atom/atom-react";import { Reactivity } from "@effect/experimental";import { Duration, Effect } from "effect";
import { atomRuntime } from "@/atom-runtime";import { type AddUserFormValues } from "@/schema/user-schema";import { UserService } from "@/services/user-service";
const baseUsersAtom = atomRuntime .atom( Effect.gen(function* () { return yield* UserService.getUsers(); }), ) .pipe(Atom.setIdleTTL(Duration.hours(1)), Atom.withReactivity(["users"]));
// ============ Users Atom ============export const usersAtom = ( typeof window !== "undefined" ? Atom.refreshOnWindowFocus(baseUsersAtom) : baseUsersAtom).pipe(Atom.setIdleTTL(Duration.hours(1)), Atom.withReactivity(["users"]));
32 collapsed lines
// ============ User Atom ============export const userAtom = Atom.family((id: string) => atomRuntime .atom( Effect.gen(function* () { return yield* UserService.getUser(id); }), ) .pipe(Atom.setIdleTTL(Duration.hours(1))),);
// ============ Delete User Atom ============export const deleteUserAtom = atomRuntime.fn( Effect.fnUntraced(function* (userId: string) { yield* UserService.deleteUser(userId); }), { reactivityKeys: ["users"] },);
// ============ Add User Atom ============export const addUserAtom = atomRuntime.fn<AddUserFormValues>()( Effect.fnUntraced(function* (user) { return yield* UserService.addUser(user); }),);
// ============ Invalidate Users Atom ============export const invalidateUsersAtom = atomRuntime.fn()( Effect.fnUntraced(function* () { yield* Reactivity.invalidate(["users"]); }),);We split the Atom into two steps. First, we create baseUsersAtom with our existing configuration. Then we conditionally wrap it with Atom.refreshOnWindowFocus.
Why the typeof window !== "undefined" check?
In Next.js, client components aren’t exclusively browser-side. When a page first loads, Next.js renders client components on the server to generate the initial HTML. Since window is a browser API that doesn’t exist on the server, Atom.refreshOnWindowFocus — which listens for browser focus events — would fail during server-side rendering. The check ensures we only apply the window focus behavior in the browser. On the server, we use the base Atom as-is.
Note that the exported name remains usersAtom, so nothing changes for the components or derived Atoms that depend on it.
Wrapping userAtom
We apply the same pattern to userAtom so that the user detail page also refreshes when the window regains focus:
22 collapsed lines
import { Atom } from "@effect-atom/atom-react";import { Reactivity } from "@effect/experimental";import { Duration, Effect } from "effect";
import { atomRuntime } from "@/atom-runtime";import { type AddUserFormValues } from "@/schema/user-schema";import { UserService } from "@/services/user-service";
const baseUsersAtom = atomRuntime .atom( Effect.gen(function* () { return yield* UserService.getUsers(); }), ) .pipe(Atom.setIdleTTL(Duration.hours(1)), Atom.withReactivity(["users"]));
// ============ Users Atom ============export const usersAtom = ( typeof window !== "undefined" ? Atom.refreshOnWindowFocus(baseUsersAtom) : baseUsersAtom).pipe(Atom.setIdleTTL(Duration.hours(1)), Atom.withReactivity(["users"]));
// ============ User Atom ============export const userAtom = Atom.family((id: string) => { const base = atomRuntime .atom( Effect.gen(function* () { return yield* UserService.getUser(id); }), ) .pipe(Atom.setIdleTTL(Duration.hours(1)));
return typeof window !== "undefined" ? Atom.refreshOnWindowFocus(base) : base;});
21 collapsed lines
// ============ Delete User Atom ============export const deleteUserAtom = atomRuntime.fn( Effect.fnUntraced(function* (userId: string) { yield* UserService.deleteUser(userId); }), { reactivityKeys: ["users"] },);
// ============ Add User Atom ============export const addUserAtom = atomRuntime.fn<AddUserFormValues>()( Effect.fnUntraced(function* (user) { return yield* UserService.addUser(user); }),);
// ============ Invalidate Users Atom ============export const invalidateUsersAtom = atomRuntime.fn()( Effect.fnUntraced(function* () { yield* Reactivity.invalidate(["users"]); }),);The conditional wrapping happens inside Atom.family, so each user’s Atom instance gets the window focus behavior individually.
Testing Window Focus Refresh
To see this in action:
- Go to the home page and wait for the users to load
- Open and move to another browser tab
- Come back to your app’s tab
Now, open the Network tab open in your browser’s dev tools, and you’ll see a new request fire to the /users endpoint. The data is being refreshed in the background.
Now test the detail page. Click on any user card, switch to another tab, and return. You’ll see a request to the individual user’s endpoint.
But from the user’s perspective, there’s no indication that a refresh is happening. The page looks exactly the same during the refetch. For a better experience, we should provide a visual cue — like dimming the existing content — so users know fresh data is on its way. In the next chapter, we’ll learn how to use the waiting property that Effect Atom provides for exactly this purpose.