On day 29, I add a loader (A <div>Loading ...</div>) to show that the page is loading the data.
It is incredibly easy in Angular 20 because it is a built-in functionality in httpResource. In Vue 3, I installed vueuse and applied the useFetch composable to initiate the network requests.
In SvelteKit, a loader can be implemented with the navigating object in $app/state. It also has an error helper function to set the HTTP status code and an error message.
| Framework | Approach |
|---|---|
| Vue 3 | vueuse's useFetch Composable |
| SvelteKit | navigating and the error helper function |
| Angular 20 | built-in in httpResource |
Implement loading and error states in Home
Vue 3 application
Install vueuse/core
npm install --save-exact @vueuse/core
Replace theusePost composable with the useFetch composable.
export const postsUrl = 'https://jsonplaceholder.typicode.com/posts'
export const usersUrl = 'https://jsonplaceholder.typicode.com/users'
<script setup lang="ts">
import PostCard from '@/components/PostCard.vue';
import { postsUrl } from '@/constants/apiEndpoints';
import type { Post } from '@/types/post';
import { useFetch } from '@vueuse/core';
const {
data: posts,
isFetching,
error,
} = useFetch<Post[]>(postsUrl).json()
</script>
The userFetch accepts a URL that is either a string or a ref/shallowRef. The .json() function returns data as a JSON.
isFetching is true when data is being loaded and falose when the loading completes.
error returns any error that occurs during the network request.
<template>
<div v-if="isFetching" class="text-center mb-10">Loading ...</div>
<div v-if="error" class="text-center mb-10">{{ error }}</div>
<div v-if="posts" class="flex flex-wrap flex-grow">
<p class="ml-2 w-full">Number of posts: {{ posts.length }}</p>
<PostCard v-for="post in posts" :key="post.id" :post="post" />
</div>
</template>
When isFetching is true, thediv element displays the Loading... static text. When there is an error, {{ error }} displays the network error. When the endpoint returns the posts successfully, the v-for directive iterates the array and renders the PostCard component.
SvelteKit application
The loading indicator and error message are displayed in +layout.svelte.
<script lang="ts">
import { page, navigating } from '$app/state';
let { children } = $props();
</script>
{#if navigating.to}
<div>Loading page...</div>
{:else if page.error}
{page.error.message}
{:else}
<div class="container">
{@render children?.()}
</div>
{/if}
When navigation.to is not null, the page is navigating and is loading the data. Therefore, the div element displays the Loading page... static text.
If page.error references an Error object, page.error.message displays the error message.
import { BASE_URL } from '$lib/constants/posts.const';
import type { Post } from '$lib/types/post';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
// retreive all posts
export const load: PageServerLoad = async ({ fetch }) => {
const postResponse = await fetch(`${BASE_URL}/posts`);
if (!postResponse.ok) {
error(404, {
message: 'Failed to fetch posts'
});
}
const posts = (await postResponse.json()) as Post[];
return { posts };
};
The load function checks that the response is not ok and the error helper function throws a 404 error with a custom message.
Angular 20 application
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { PostcardComponent } from '../post/postcard.component';
import { PostsService } from '../post/services/posts.service';
@Component({
selector: 'app-home',
imports: [PostcardComponent],
template: `... inline template ...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HomeComponent {
postService = inject(PostsService);
postsRes = this.postService.posts;
posts = computed(() => (this.postsRes.hasValue() ? this.postsRes.value() : []));
error = computed<string>(() =>
this.postsRes.status() === 'error' ? 'Error loading the posts.' : '',
);
}
When the postsRes resource's status is error, the error computed signal returns a custom error message. Otherwise, the error message is an empty string.
@if (postsRes.isLoading()) {
<div>Loading...</div>
} @else if (error()) {
<div>Error: {{ error() }}</div>
}
@if (posts(); as allPosts) {
<div class="flex flex-wrap flex-grow">
<p class="ml-2 w-full">Number of posts: {{ allPosts.length }}</p>
@for (post of allPosts; track post.id) {
<app-postcard [post]="post" />
}
</div>
}
When postsRes.isLoading() is true, the div element displays the Loading... static text.
When the error computed signal is evaluated to true, the error message is displayed.
When allPosts is defined, the template displays the number of posts and iterates the array to render the PostComponent.
Implement loading and error states in Post
Vue 3 application
First, the useFetch composable retrieves a post by the path param. This is straightforward because this url depends on path param only and it can be destructured from the useRoute composable.
<script setup lang="ts">
import { postsUrl, usersUrl } from '@/constants/apiEndpoints'
import type { Post } from '@/types/post'
import type { User } from '@/types/user'
import { useFetch } from '@vueuse/core'
import { computed, shallowRef, watch } from 'vue'
import { useRoute } from 'vue-router'
const { params } = useRoute()
const url = `${postsUrl}/${params.id}`
const {
data: post,
isFetching: isFetchingPost,
error: errorPost
} = useFetch<Post>(url).json()
</script>
When post is retrieved, use the useFetch composable to retrieve the user by the post's user ID.
The user url changes when the user ID changes, so it is a shallowRef.
const userUrl = shallowRef('')
const {
data: user,
isFetching: isFetchingUser,
error: errorUser,
} = useFetch<User>(userUrl, { refetch: true, immediate: false }).json()
The refetch: true means that a new request is made when userUrl changes. Moreover, the userUrl is initially a blank string, so I don't want it to fire immediately. The final useFetchOptions is:
{
refetch: true,
immediate: true,
}
Modify the watcher to track the post and update the userUrl shallowRef programmatically.
watch(
() => ({ ...post.value }),
({ userId = undefined }) => (userUrl.value = userId ? `${usersUrl}/${userId}` : ''),
)
When userUrl is not blank, the useFetch composable automatically retrieves a user by the user ID.
const isFetching = computed(() => isFetchingPost.value || isFetchingUser.value)
Add the isFetching computed ref to display a loader when post or user is being loaded.
const error = computed(() => {
if (errorPost.value) {
return errorPost instanceof Error ? errorPost.message : 'Error retrieving a post.'
}
if (errorUser.value) {
return errorUser instanceof Error ? errorUser.message : 'Error retrieving a user.'
}
return ''
})
Add the error computed ref to display any error message.
<template>
<div v-if="isFetching" class="text-center my-10">Loading...</div>
<div v-if="error" class="text-center my-10">{{ error }}</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>
Display a loader when isFetching is true and an error message when it is not blank. The template displays the post and user name after both data is loaded successfully.
SvelteKit application
import { BASE_URL } from '$lib/constants/posts.const';
import type { Post } from '$lib/types/post';
import type { PostWitUser, User } from '$lib/types/user';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
// retreive a post by an ID
export const load: PageServerLoad = async ({ params, fetch }): Promise<PostWitUser> => {
const post = (await retrieveResource(fetch, `posts/${+params.id}`, 'Post')) as Post;
const user = (await retrieveResource(fetch, `users/${post.userId}`, 'User')) as User;
return {
post,
user,
};
};
The retrieveResource helper function uses the native fetch function to retrieve the post by the post ID. When the post is retrieved successfully, this helper function uses the post's user ID to retrieve the user. Next, the load function returns both the post and user to +page.svelte.
type FetchFunction = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
async function retrieveResource(fetch: FetchFunction, subPath: string, itemName: string) {
const url = `${BASE_URL}/${subPath}`;
const response = await fetch(url);
if (!response.ok) {
error(404, {
message: `Failed to fetch ${itemName}`
});
}
const item = await response.json();
if (!item) {
error(404, {
message: `${itemName} does not exist`
});
}
return new Promise((resolve) => {
setTimeout(() => resolve(item), 1000);
});
}
The retrieveResource fetches the item and examines the response. When the response is not ok, it throws a 404 error with the custom message, Failed to fetch ${itemName}. await response.json() resolves the Promise to an object. If the object is undefined, it also throws a 404 error with the custom message, ${itemName} does not exist.
return new Promise((resolve) => {
setTimeout(() => resolve(item), 1000);
});
The promise creates a delay of one second to simulate the loading behavior.
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({
selector: 'app-post',
styles: `
@reference "../../styles.css";
:host {
@apply flex m-2 gap-2 items-center w-1/4 flex-grow rounded overflow-hidden w-full;
}
`,
template: `... inline template ...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
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));
error = computed<string>(() =>
this.userRef.status() === 'error' ? 'Error loading the post.' : '',
);
}
@let myUser = user();
@let myPost = post();
@if (userRef.isLoading()) {
<div>Loading...</div>
} @else if (error()) {
<div>Error: {{ error() }}</div>
} @else if (myPost && myUser) {
<div class="mb-10">
<h1 class="text-3xl">{{ myPost.title }}</h1>
<div class="text-gray-500 mb-10">by {{ myUser.name }}</div>
<div class="mb-10">{{ myPost.body }}</div>
</div>
} @else {
<div>Post not found</div>
}
When userRef.isLoading() is true, the div element displays the Loading... static text.
When the error computed signal is evaluated to true, the error message is displayed.
The last elseif displays the post title, post body and user name.
We have successfully implemented a simple loader and an error indicator on each page.
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
- Sveltekit Errors: https://svelte.dev/docs/kit/errors
- Sveltekit Navigating: https://svelte.dev/tutorial/kit/navigating-state
- useFetch composable: https://vueuse.org/core/useFetch/
Top comments (0)