Refreshing Data on Window Focus

Avatar of Hemanta SundarayHemanta Sundaray

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:

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";
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.

atoms/user.ts
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.

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.

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.