So far, the UserGrid component handles loading states by checking the result’s state. When data is loading, it renders a spinner. When data is ready, it renders the users.
This approach works, but it means every component that consumes Atom data needs to handle loading states explicitly. React provides a cleaner pattern for this: Suspense.
Effect Atom integrates with React Suspense through the useAtomSuspense hook. Instead of returning the loading state to your component, useAtomSuspense suspends the component until the data is ready. You then wrap your component in a <Suspense> boundary and provide a fallback UI.
When a component “suspends,” it tells React: “I’m not ready to render yet.” React stops rendering that component and looks up the component tree for the nearest <Suspense> boundary. It then renders that boundary’s fallback prop until the component is ready. Once the data arrives, React renders the component with the data. The component never sees the loading state — it either renders with data or doesn’t render at all.
Creating a Suspense-Enabled UserGrid Component
Create a new file components/user-grid-suspense.tsx:
"use client";
import { useAtomSuspense } from "@effect-atom/atom-react";
import { UserSuccessCard } from "@/components/user-success-card";
import { usersAtom } from "@/atoms/user";
export function UserGridSuspense() { const result = useAtomSuspense(usersAtom);
return ( <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> {result.value.users.map((user) => ( <UserSuccessCard key={user.id} user={user} /> ))} </div> );}Notice how much simpler this component is. There’s no need to check for loading states. useAtomSuspense suspends the component until the data is available. When the component renders, we know we have data: result is always a Success, and result.value is the users array.
Adding the Suspense Boundary
Update app/page.tsx as shown below:
"use client";
import { Suspense } from "react";import { Link } from "react-transition-progress/next";
import { UserGrid } from "@/components/user-grid";import { UserGridSpinner } from "@/components/user-grid-spinner";import { UserGridSuspense } from "@/components/user-grid-suspense";import { UserPagination } from "@/components/user-pagination";import { UserSearchBar } from "@/components/user-search-bar";
export default function HomePage() { return ( <div className="container max-w-5xl space-y-10"> <div className="flex items-center justify-between"> <h1>Users</h1> <Link href="/add-user" className="bg-accent text-foreground px-4 py-2 text-sm font-medium transition-colors hover:bg-neutral-200" > + Add User </Link> </div> <UserSearchBar /> <Suspense fallback={<UserGridSpinner />}> <UserGridSuspense /> </Suspense> <UserPagination /> </div> );}The <Suspense> boundary catches the suspension from useAtomSuspense and renders<UserGridSpinner /> while the data loads. Once the data is ready, React renders UserGridSuspense with the users.
The home page behaves exactly as before. The spinner shows while data loads, then the user cards appear. The difference is structural: the loading logic now lives in the Suspense boundary rather than inside the component.