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 { Effect, Layer, ServiceMap } from "effect";import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse,} from "effect/unstable/http";
import { apiBaseUrlConfig } from "@/lib/config";import { USERS_PER_PAGE } from "@/lib/constants";import { mapHttpError } from "@/lib/http-error";import { ParseError, type HttpError } from "@/errors";import { UserSchema, UsersSchema, type User, type UsersResponse,} from "@/schema/user-schema";
export class UserService extends ServiceMap.Service<UserService>()( "app/UserService", { make: Effect.gen(function* () { const client = (yield* HttpClient.HttpClient).pipe( HttpClient.filterStatusOk, );
const apiBaseUrl = yield* apiBaseUrlConfig;
31 collapsed lines
// ============ Get Users ============ function getUsers(): Effect.Effect<UsersResponse, HttpError> { 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.catchTag("HttpClientError", (error) => Effect.fail(mapHttpError(error)), ), Effect.catchTag("SchemaError", (error) => Effect.fail( new ParseError({ message: "Received an unexpected response from the server.", cause: error, }), ), ), ); }
// ============ Get User ============ function getUser(id: string): Effect.Effect<User, HttpError> { return client.get(`${apiBaseUrl}/users/${id}`).pipe( Effect.delay("1 second"), Effect.flatMap(HttpClientResponse.schemaBodyJson(UserSchema)), Effect.catchTag("HttpClientError", (error) => Effect.fail(mapHttpError(error)), ), Effect.catchTag("SchemaError", (error) => Effect.fail( new ParseError({ message: "Received an unexpected response from the server.", cause: error, }), ), ), ); }
return { getUsers, getUser }; }), },) { static layer = Layer.effect(this, this.make).pipe( Layer.provide(FetchHttpClient.layer), );}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 (the list of all users) every time. 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 { Duration, Effect } from "effect";import { Atom } from "effect/unstable/reactivity";
import { atomRuntime } from "@/atom-runtime";import { UserService } from "@/services/user-service";
// ============ Users Atom ============export const usersAtom = atomRuntime .atom( Effect.gen(function* () { const userService = yield* UserService; return yield* userService.getUsers(); }), ) .pipe(Atom.setIdleTTL(Duration.hours(1)));
// ============ 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))),);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 { useAtomValue } from "@effect/atom-react";import { AsyncResult } from "effect/unstable/reactivity";
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 AsyncResult.builder(result) .onInitial(() => <UserGridSpinner />) .onError((error) => { const { title, message } = getErrorInfo(error); return <FailureCard title={title} message={message} />; }) .onDefect((defect) => { const { title, message } = getErrorInfo(defect); 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 AsyncResult.builder.
Click the View Details link on any user card. You’ll be redirected to the user details page where you’ll see the user’s information. If you navigate back and revisit the same user, you won’t see the loading spinner because the data is already cached.