Currently, we’re rendering the raw structure of the Result object to visualize its internal state. Now that we understand the three states a Result can be in, let’s replace the JSON dump with a proper UI.
What does a proper UI for async state look like?
We need to account for three states:
- Initial: Data fetching is in progress. We show a spinner.
- Failure: Data fetching failed. We show the error name and message.
- Success: Data fetching succeeded. We render the users.
Replace the existing code inside components/user-grid.tsx with the following:
"use client";
import { Result, useAtomValue } from "@effect-atom/atom-react";import { Cause } from "effect";
import { FailureCard } from "@/components/failure-card";import { UserEmptyCard } from "@/components/user-empty-card";import { UserGridSpinner } from "@/components/user-grid-spinner";import { UserSuccessCard } from "@/components/user-success-card";
import { usersAtom } from "@/atoms/user";import { ConfigError, GetUsersError } from "@/errors";import { UsersResponse } from "@/schema/user-schema";import { getErrorInfo } from "@/lib/utils";
const selectUsers = ( result: Result.Result<UsersResponse, ConfigError | GetUsersError>,) => Result.map(result, (data) => data.users);
export function UserGrid() { const usersResult = useAtomValue(usersAtom, selectUsers);
return Result.match(usersResult, { onInitial: () => <UserGridSpinner />,
onFailure: (failure) => { const error = Cause.squash(failure.cause); const { title, message } = getErrorInfo(error); return <FailureCard title={title} message={message} />; },
onSuccess: (success) => { const users = success.value;
if (users.length === 0) { return <UserEmptyCard reason="empty-users-list" />; }
return ( <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-10"> {users.map((user) => ( <UserSuccessCard key={user.id} user={user} /> ))} </div> ); }, });}If you see a React hooks order error after saving, do a full page refresh.
This happens because the previous version of UserGrid used useAtomValue
without a transform function, and the new version uses one. The different code
paths call different hooks, which triggers a React warning during hot reload.
Let’s understand what’s going on here.
The UserGrid component needs the users array. But usersAtom contains both the users array and the users count. So, we pass a transform function named selectUsers as the second argument to useAtomValue. selectUsers takes in the full Result and uses Result.map to extract just the users array. This way, usersResult contains only the users.
Result.match takes a Result and an object with three handlers, one for each state:
-
onInitialrenders a spinner while data is being fetched. -
onSuccessreceives the success value (theusersarray) and renders the user cards, or an empty state if the array is empty. -
onFailuredeserves a closer look. The failure result contains acauseproperty, but this isn’t the error directly. It’s an EffectCause, which is a wrapper that can represent complex error scenarios like multiple failures or interruptions.Cause.squashextracts the underlying error from this wrapper. Once we have the error, we pass it togetErrorInfo, which reads the error’s name and message to produce a user-friendly title and description for theFailureCard.
Result.match guarantees exhaustiveness. TypeScript will error if you forget
to handle any of the three states. Try commenting out one of the handlers to
see this in action.
Now, visit the home page. You should see 12 user cards rendered. To see the FailureCard in action, go to user-service.ts and change the endpoint from /users to /usersx (which doesn’t exist). Visit the home page again. You should see the failure card rendered with the error title and message. Remember to revert the endpoint back to /users before continuing.