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.
Our app doesn’t strictly need this feature because the users’ data doesn’t change frequently. Window focus refresh is most useful in apps where data changes frequently, such as a dashboard displaying live metrics. But it’s a common pattern worth knowing, so let’s learn 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.
Adding Window Focus Refresh to usersAtom
Update atoms/user.ts as shown below:
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";
const baseUsersAtom = atomRuntime .atom(() => Effect.gen(function* () { const userService = yield* UserService; 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"]));
35 collapsed lines
// ============ 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"]); }),);We split the Atom into two parts. 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.
Adding Window Focus Refresh to usersAtom
We apply the same pattern to userAtom so that the user detail page also refreshes when the user switches back to the tab.
22 collapsed lines
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";
const baseUsersAtom = atomRuntime .atom(() => Effect.gen(function* () { const userService = yield* UserService; 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* () { const userService = yield* UserService; return yield* userService.getUser(id); }), ) .pipe(Atom.setIdleTTL(Duration.hours(1)));
return typeof window !== "undefined" ? Atom.refreshOnWindowFocus(base) : base;});
23 collapsed lines
// ============ 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"]); }),);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 refetched in the background.
Now test the detail page. Click the View details link 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 flag that Effect Atom provides for this purpose.