Currently, if you click the View Details link on any user card, you get redirected to the user’s detail page. But on that page, you just see the text User Detail. We need to fetch the specific user’s data and render their details.
Adding the getUser Method
First, add the getUser function to your UserService in services/user-service.ts:
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse,} from "@effect/platform";import { Effect } from "effect";
import { apiBaseUrlConfig } from "@/lib/config";import { USERS_PER_PAGE } from "@/lib/constants";import { ConfigError, GetUserError, GetUserParseError, GetUserRequestError, GetUserResponseError, GetUsersError, GetUsersParseError, GetUsersRequestError, GetUsersResponseError,} from "@/errors";import { User, UserSchema, UsersSchema, type UsersResponse,} from "@/schema/user-schema";
export class UserService extends Effect.Service<UserService>()( "app/UserService", { effect: Effect.gen(function* () { const client = (yield* HttpClient.HttpClient).pipe( HttpClient.filterStatusOk, );
const apiBaseUrl = yield* apiBaseUrlConfig.pipe( Effect.catchTag("ConfigError", (error) => Effect.fail( new ConfigError({ message: "API base URL is not configured.", cause: error, }), ), ), );
44 collapsed lines
// ============ Get Users ============ function getUsers(): Effect.Effect<UsersResponse, GetUsersError> { const request = HttpClientRequest.get(`${apiBaseUrl}/users`).pipe( HttpClientRequest.setUrlParams({ _limit: USERS_PER_PAGE.toString(), }), );
return client.execute(request).pipe( Effect.delay("1 second"), Effect.flatMap((response) => Effect.all({ users: HttpClientResponse.schemaBodyJson(UsersSchema)(response), usersCount: Effect.succeed( Number(response.headers["x-total-count"]), ), }), ), Effect.catchTags({ RequestError: (requestError) => Effect.fail( new GetUsersRequestError({ message: "Failed to get users", cause: requestError, }), ), ResponseError: (responseError) => Effect.fail( new GetUsersResponseError({ message: `Failed to get users: status ${responseError.response.status}`, cause: responseError, }), ), ParseError: (parseError) => { return Effect.fail( new GetUsersParseError({ message: "Failed to parse getUsers response", cause: parseError, }), ); }, }), ); }
// ============ Get User ============ function getUser(id: string): Effect.Effect<User, GetUserError> { return client.get(`${apiBaseUrl}/users/${id}`).pipe( Effect.delay("1 second"), Effect.flatMap(HttpClientResponse.schemaBodyJson(UserSchema)), Effect.catchTags({ RequestError: (requestError) => Effect.fail( new GetUserRequestError({ message: `Failed to get user ${id}`, cause: requestError, }), ), ResponseError: (responseError) => Effect.fail( new GetUserResponseError({ message: `Failed to get user ${id}: status ${responseError.response.status}`, cause: responseError, }), ), ParseError: (parseError) => Effect.fail( new GetUserParseError({ message: "Failed to parse getUser response", cause: parseError, }), ), }), ); }
return { getUsers, getUser }; }), dependencies: [FetchHttpClient.layer], accessors: true, },) {}This follows the same pattern as getUsers, but fetches a single user by ID instead of a list.
Why We Need Atom.family
With usersAtom, we fetch the same data every time — the list of all users. There’s only one version of this data, so a single Atom works perfectly.
But a user detail page is different. Each user has a unique ID, and we need to fetch different data depending on which user we’re viewing. User 1’s data is different from User 3’s data. We can’t use a single Atom because the data depends on which user ID we pass in.
This is exactly what Atom.family solves. It creates a factory function that produces a separate Atom for each unique parameter:
- userAtom(“1”): creates an Atom that fetches user 1’s data
- userAtom(“1”): returns the same Atom (already created)
- userAtom(“3”): creates a different Atom for user 3
Think of Atom.family as a cache of Atoms keyed by their parameters. Each user’s data is fetched and cached independently. If you view user 1, navigate away, then come back, the data loads instantly from cache (assuming the TTL hasn’t expired). Meanwhile, user 3’s data remains unaffected.
Creating the User Atom
Now create the user Atom in atoms/user.ts:
import { Atom } from "@effect-atom/atom-react";import { Duration, Effect } from "effect";
import { atomRuntime } from "@/atom-runtime";import { UserService } from "@/services/user-service";
// ============ Users Atom ============export const usersAtom = atomRuntime .atom( Effect.gen(function* () { return yield* UserService.getUsers(); }), ) .pipe(Atom.setIdleTTL(Duration.hours(1)));
// ============ User Atom ============export const userAtom = Atom.family((id: string) => atomRuntime .atom( Effect.gen(function* () { return yield* UserService.getUser(id); }), ) .pipe(Atom.setIdleTTL(Duration.hours(1))),);We pass Atom.family a function that receives an id and returns an Atom. Each unique id gets its own Atom instance with its own cache and TTL.
Displaying User Details
Now replace the code inside components/user-detail.tsx with the following:
"use client";
import { useParams } from "next/navigation";import { Result, useAtomValue } from "@effect-atom/atom-react";
import { userAtom } from "@/atoms/user";
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";
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) => <UserDetailsCard user={user} />) .render();}We extract the id from the URL parameters using Next.js’s useParams hook, then pass it to userAtom(params.id). This gives us the Atom instance for that specific user. The rest follows the same pattern we used in UserGrid. We handle loading, error, and success states using Result.builder.
Click the View Details link on any user card. You’ll be redirected to the user details page where the user’s information loads and displays. Navigate back to the home page, then click a different user. Each user’s data is fetched and cached independently.