In the previous chapter, we saw that switching to another tab and returning triggers a data refetch. But from the user’s perspective, there’s no indication that a refresh is happening. The page looks exactly the same during the refetch. A common pattern for this scenario is to dim the existing content, giving users a subtle visual cue that fresh data is on its way.
The Waiting Property
When an effectful Atom refetches, useAtomSuspense does not suspend again by default. The component keeps rendering with the previous data while fresh data loads in the background. This is the behavior we want, but we need a way to know that a refetch is in progress.
The result returned by useAtomSuspense has a waiting property. When waiting is true, the Atom is refetching in the background. When the fetch completes, waiting becomes false.
If you’d prefer the component to suspend during refetches — showing the Suspense fallback instead of stale data — you can pass { suspendOnWaiting: true } to useAtomSuspense. This is useful when you want to guarantee users always see completely fresh data. For our app, we’ll keep showing stale data with a visual indicator.
Updating the User Grid
Update components/user-grid-suspense.tsx to pass the waiting boolean to each card:
"use client";
import { useAtomRefresh, useAtomSuspense } from "@effect-atom/atom-react";import { Cause } from "effect";
import { FailureCard } from "@/components/failure-card";import { UserSuccessCard } from "@/components/user-success-card";
import { getErrorInfo } from "@/lib/utils";import { usersAtom } from "@/atoms/user";
export function UserGridSuspense() { const result = useAtomSuspense(usersAtom, { includeFailure: true }); const refresh = useAtomRefresh(usersAtom);
if (result._tag === "Failure") { const error = Cause.squash(result.cause); const { title, message } = getErrorInfo(error);
return <FailureCard title={title} message={message} onRetry={refresh} />; }
return ( <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> {result.value.map((user) => ( <UserSuccessCard key={user.id} user={user} waiting={result.waiting} /> ))} </div> );}result.waiting is true whenever the Atom is refetching in the background. We pass it to each UserSuccessCard. The card component is already configured to reduce its opacity when waiting is true, dimming the content to signal that fresh data is loading.
Testing the User Grid
- Switch to a different browser tab.
- Switch back to your app’s tab.
You’ll see the user cards briefly dim as the data refetches. Once the fresh data arrives, the cards return to full opacity.
Updating the User Detail Page
The same pattern applies to the user detail page. On this page, we render the UserDetail component, which uses Result.builder to handle the different states.
In the onSuccess handler, the second parameter is the Result itself, which includes the waiting boolean. Update components/user-detail.tsx to extract it and pass it to UserDetailsCard:
"use client";
import { useParams } from "next/navigation";import { Result, useAtomValue } from "@effect-atom/atom-react";
import { FailureCard } from "@/components/failure-card";import { UserDetailsCard } from "@/components/user-details-card";import { UserGridSpinner } from "@/components/user-grid-spinner";
import { getErrorInfo } from "@/lib/utils";import { userAtom } from "@/atoms/user";
export function UserDetail() { const params = useParams<{ id: string }>(); const result = useAtomValue(userAtom(params.id));
return Result.builder(result) .onInitial(() => <UserGridSpinner />) .onFailure((cause) => { const { title, message } = getErrorInfo(cause); return <FailureCard title={title} message={message} />; }) .onSuccess((user, { waiting }) => ( <UserDetailsCard user={user} waiting={waiting} /> )) .render();}Like UserSuccessCard, UserDetailsCard is already configured to reduce its opacity when waiting is true.
Testing the User Detail Page
- Wait for the user details to load.
- Switch to a different browser tab.
- Switch back to your app’s tab.
You’ll see the user details card dim briefly as the data refetches, then return to full opacity when the fresh data arrives.