Fetching Data by ID

Avatar of Hemanta SundarayHemanta Sundaray

Adding the getUser Method

First, add the getUser function to your UserService in services/user-service.ts:

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:

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:

components/user-detail.tsx
"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.

Sign in to save progress

Stay in the loop

Get notified when new chapters are added and when this course is complete.