On day 28, I use the userId of a post to call the users endpoint to retrieve the user name.
In Vue 3, I created another composable to return a user Object with a name property. In Angular 20, it is a User service that uses the experimental httpResource to retrieve a user when the post is updated. In SvelteKit, I added new logic in the load function to query a user using the userId of a post.
| Framework | Approach |
|---|---|
| Vue 3 | useUser Composable |
| SvelteKit | Make a new request to the users endpoint to retrieve a user by user id |
| Angular 20 | A user httpResource to retrieve a user by user id in a service |
Post Types
Create a User type to retrieve id, name.
export type User = {
id: number;
name: string;
}
Load User data
Vue 3 application
Create auseUser composable under src/composables folder.
Implement a fetchOne function and declare a user ref to retrieve a user by an ID.
import type { User } from '@/types/user'
import { ref } from 'vue'
export function useUser() {
const user = ref<User | null>(null)
const baseUrl = `https://jsonplaceholder.typicode.com/users/${id}`
function fetchOne(id: number, signal: AbortSignal) {
return fetch(baseUrl, { signal })
.then((response) => response.json() as Promise<User>)
.then((data) => (user.value = data))
}
return {
user,
fetchOne,
}
}
The fetchOne function makes a request to the baseUrl to retrieve a user and assign the user touser.value.
Then, the composable returns both user and fetchOne so the Post component can accesss them.
SvelteKit application
I modified the code to use async/await to retrieve a post and the post user.
export type PostWitUser = {
post: Post | undefined;
user: User | undefined;
};
PostWithUser is a new type that holds the data of a post and its author.
import { BASE_URL } from '$lib/constants/posts.const';
import type { Post } from '$lib/types/post';
import type { PostWitUser, User } from '$lib/types/user';
import type { PageLoad } from './$types';
// retreive a post by an ID
export const load: PageLoad = async ({ params, fetch }): Promise<PostWitUser> => {
console.log('params', params);
try {
const response = await fetch(`${BASE_URL}/posts/${params.id}`);
const post = (await response.json()) as Post;
const userResponse = post ? await fetch(`${BASE_URL}/users/${post?.userId}`) : undefined;
const user = userResponse ? ((await userResponse.json()) as User) : undefined;
return {
post,
user
};
} catch (error) {
console.error('Error fetching a post:', error);
return {
post: undefined,
user: undefined
};
}
};
When the first request retrieves a post successfully, the second request uses the userId to retrieve the user.
Angular 20 application
Install zod to parse the result of the httpResource.
npm install --save-exact zod
import z from 'zod';
export const userSchema = z.object({
id: z.number(),
name: z.string(),
});
export type User = z.infer<typeof userSchema>;
Create a userSchema and infer the User type.
import { httpResource } from '@angular/common/http';
import { Injectable, Signal } from '@angular/core';
import { Post } from '../types/post.type';
import { User, userSchema } from '../types/user.type';
const BASE_URL = 'https://jsonplaceholder.typicode.com/users';
@Injectable({
providedIn: 'root',
})
export class UserService {
createUserResource(post: Signal<Post | undefined>) {
return httpResource<User>(
() => {
const result = post();
return result ? `${BASE_URL}/${result.userId}` : undefined;
},
{
defaultValue: undefined,
equal: (a, b) => a?.id === b?.id,
parse: userSchema.parse,
},
).asReadonly();
}
}
This is a technique to create an httpResource in a service. The createUserResource method accepts a post signal to track its changes. When the post is updated, the httpResource makes an HTTP request to retrieve a user the user id.
{
defaultValue: undefined,
equal: (a, b) => a?.id === b?.id,
parse: userSchema.parse,
}
The default value is undefined. Two users are considered equal when they have the same ID and no HTTP request. userSchema.parse parses the raw data to a User object.
Replace the User mock data with a fetch call
Vue 3 application
In the Post view, import the usePost composable and destructure fetch and user.
In the Post component, import usePost and invoke fetchOne to retrieve a post by an ID.
import { useRoute } from 'vue-router'
import { usePost } from '@/composables/usePost'
import { useUser } from '@/composables/useUser'
const { post, fetchOne } = usePost()
const { user, fetchOne: fetchUser } = useUser()
const { params } = useRoute();
fetchOne(+params.id)
fetchUser(post?.userId)
<template>
<div v-if="isLoading" class="text-center my-10">
Loading...
</div>
<div v-if="post && user" class="mb-10">
<h1 class="text-3xl">{{ post.title }}</h1>
<div class="text-gray-500 mb-10">by {{ user.name }}</div>
<div class="mb-10">{{ post.body }}</div>
</div>
</template>
In the template, replace the hardcoded name with {{ user.name }}.
SvelteKit application
<script lang="ts">
import type { PageProps } from './$types';
const { data }: PageProps = $props();
const { post, user } = data;
</script>
In posts/[id]/+page.svelte, the load function retrieves a post by an ID and the user by the post's userId.
<div class="mb-10">
<h1 class="text-3xl">{ post?.title }</h1>
<div class="text-gray-500 mb-10">by { user?.name }</div>
<div class="mb-10">{ post?.body }</div>
</div>
Now, the template can display the user name by interpolating { user?.name }.
Angular 20 application
import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
import { UserService } from './services/user.service';
import { Post } from './types/post.type';
@Component({
... no change ...
})
export default class PostComponent {
readonly userService = inject(UserService);
post = input<Post>();
userRef = this.userService.createUserResource(this.post);
user = computed(() => (this.userRef.hasValue() ? this.userRef.value() : undefined));
}
The PostComponent injects the userService and calls the createUserResource method to create a userRef.
user is a computed signal that computes the user value. When the userRef has value, this.userRef.value() returns a valid User object. Otherwise, user gets undefined.
No change to the inline template.
Fix the User bug (Vue 3 only)
At this point, there is a bug in the Post view where the user is undefined. It happens when fetchUser(post?.userId) is invoked before the post data arrives. The fix is to use watch to track changes to the post ref. When the ref receives a new value, the watcher callback calls the fetchUser function to retrieve the user name.
watch(() => ({ ...post.value }), (newPost, oldPost, onCleanup) => {
const controller = new AbortController()
if (newPost?.userId) {
fetchUser(newPost.userId, controller.signal)
}
onCleanup(() => {
controller.abort()
})
})
Pass a getter function to watch to track updates to the post. When the new post has a user id, it is used to fetch a new user to be displayed.
We have successfully displayed the user of the post.
Github Repositories
- Vue 3: https://github.com/railsstudent/vue-example-blog
- Svelte 5: https://github.com/railsstudent/svelte-example-blog
- Angular 20: https://github.com/railsstudent/angular-example-blog
Resources
- JSON Placeholder API: https://jsonplaceholder.typicode.com/
- Basic SvelteKit Tutorial: https://svelte.dev/tutorial/kit/page-data
Top comments (0)