If you've worked on a React project, chances are you have fetched data from APIs. But have you never asked yourself if your way of doing was correct?
Don't get me wrong, when i say "correct", i don't mean that i have the perfect solution to this but we will attempt to eradicate data mismatch by validating it before consuming it.
In order to do that, we will use two main tools: TanStack Query and Zod
TanStack Query or React Query
What is React Query?
React Query is a powerful library for managing data in React applications. It offers an elegant and efficient way to handle complex data fetching, caching, and synchronization, allowing to build responsive and high-performing applications with ease.
Why should you use it?
One of the major benefits of React Query is its ability to cache data. This means that once data is fetched from an API, it is stored in memory and can be accessed quickly and efficiently. This is especially useful for applications that rely heavily on data fetching, as it helps to reduce the load on the network and improve performance
React Query is also easy to integrate into existing React applications. It works seamlessly with other popular libraries such as Redux and MobX, and can be easily integrated into a wide range of different architectures and design patterns.
Zod
Zod is a powerful TypeScript schema validation library that allows to define and validate data schemas with ease. It provides a simple and intuitive API for validating data, ensuring that it conforms to a specific set of rules and constraints. This makes easier for us to catch errors and bugs early in the development process.
TypeScript + React Query + Zod = Peace of mind π
Ok, enough theory, let's get down to practice. First, let's create a simple TypeScript React Vite app but running:
> pnpm create vite
> β Project name: datafetching
> β Select a framework: βΊ React
> β Select a variant: βΊ TypeScript
Let's install our dependencies:
> pnpm add zod @tanstack/react-query
For our example, we'll use the Dev.to API which will return every article a user has published. In our case, this API will return every article i wrote (go read them if you haven't yet π).
OK, let's see what we got here:
Our first task is to create types for this json response. I found this website that can generate it for us. Just paste the json result response and it will generate interfaces for you, i'll just change interface
by type
, but honestly you can leave it as it is (let's not talk about why i've made this change as it's not the main concern of this article).
And this is what we get:
As you can see, we will have two interfaces: Article and User. Let's create a folder named types inside our src folder and create two respective files: article.ts and user.ts.
// user.ts
export type User = {
name: string;
username: string;
twitter_username: string;
github_username: string;
user_id: number;
website_url: string;
profile_image: string;
profile_image_90: string;
}
// article.ts
import { User } from "./user";
export type Articles = {
type_of: string;
id: number;
title: "string;"
description: "string;"
readable_publish_date: string;
slug: string;
path: string;
url: string;
comments_count: number;
public_reactions_count: number;
collection_id?: null;
published_timestamp: string;
positive_reactions_count: number;
cover_image: string;
social_image: string;
canonical_url: string;
created_at: string;
edited_at?: string | null;
crossposted_at?: null;
published_at: string;
last_comment_at: string;
reading_time_minutes: number;
tag_list?: (string)[] | null;
tags: string;
user: User;
}
Ok, let's head back to our App.tsx file and delete everything:
function App() {
return <div></div>;
}
export default App;
Good! Now, our main goal is to fetch and display data from our API using React Query and Zod. Let's start by initializing our react query client. Open main.tsx and make this changes:
// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// Create a client
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
We can now use the React Query inside our App.tsx
import { useQuery } from "@tanstack/react-query";
function App() {
const getArticles = () => {};
const { data, isLoading, isFetching, error, isError } = useQuery({
queryKey: ["articles"], // query key that will help to cache data, you can set it whatever you like
queryFn: getArticles, // the function that will be executed to actually fetch the data
});
return <div></div>;
}
export default App;
Let's try to implement the getArticles
function to actually fetch the data.
Naive approach
const { VITE_BASE_URL } = import.meta.env;
const getArticles = (): Promise<Article[]> =>
fetch(VITE_BASE_URL).then((res) => res.json());
Of course, here we are using an env key that will be loaded from .env file that you need to create.
// .env
// If you want, you can replace the username by yours to fetch your articles published on [Dev.to](https://dev.to/)
VITE_BASE_URL=https://dev.to/api/articles?username=lynxgsm
Here is our final App.tsx file:
import { useQuery } from "@tanstack/react-query";
import { Article } from "./types/article";
import { z } from "zod";
function App() {
const { VITE_BASE_URL } = import.meta.env;
const getArticles = (): Promise<Article[]> =>
fetch(VITE_BASE_URL).then((res) => res.json());
const { data, isLoading, isFetching, error, isError } = useQuery({
queryKey: ["articles"], // a key that will help to retrieve data
queryFn: getArticles, // the function that will be executed to actually fetch the data
});
if (isLoading) {
return <p>Data is loading...</p>;
}
if (isFetching) {
return <p>Data is fetching...</p>;
}
if (error) {
return <p>There was an error when fetching your data.</p>;
}
return (
<ul>
{data?.map((article) => {
return <li key={article.id}>{article.title}</li>;
})}
</ul>
);
}
export default App;
Let's start our app and see that our data has been fetched correctly.
Ok, you've probably used this way of fetching data before and you are yelling at your computer right now that there is nothing wrong with it.
But trust me on this one: you are not doing it the right way. Let me explain, you are telling your TypeScript code that the data returned is of type Array of Article which is not correct.
Let's try to add another key inside our User type:
// user.ts
export type User = {
name: string;
username: string;
twitter_username: string;
github_username: string;
user_id: number;
website_url: string;
profile_image: string;
profile_image_90: string;
avatar: string; // This is the new key
}
We've added the avatar key although it doesn't exists in our data. Then, let's try to display it:
// App.tsx
return (
<ul>
{data?.map((article) => {
return <li key={article.id}>{article.title} - {article.user.avatar}</li>;
})}
</ul>
);
Let's rerun our App:
As you can see, even though the data doesn't exist, TypeScript will try to display it without any error. And that may be problematic if you are doing some operations with the incorrect field.
That's where Zod comes in action.
Validating data with Zod
Zod is validating data with schemas. Let's create a folder named schemas inside src and create user.ts
and article.ts
.
// user.ts
import { z } from 'zod'
export const UserSchema = z.object({
name: z.string(),
username: z.string(),
twitter_username: z.string(),
github_username: z.string(),
user_id: z.number(),
website_url: z.string(),
profile_image: z.string(),
profile_image_90: z.string()
})
We are defining the user schema with Zod. User is an object with keys and different value types. Now let's see the article.ts
:
// article.ts
import { z } from 'zod'
import { UserSchema } from './user'
export const ArticleSchema =
z.object({
type_of: z.string(),
id: z.number(),
title: z.string(),
description: z.string(),
readable_publish_date: z.string(),
slug: z.string(),
path: z.string(),
url: z.string(),
comments_count: z.number(),
public_reactions_count: z.number(),
published_timestamp: z.string(),
collection_id: z.string(),
positive_reactions_count: z.number(),
cover_image: z.string(),
social_image: z.string(),
canonical_url: z.string(),
created_at: z.string(),
edited_at: z.string(),
published_at: z.string(),
last_comment_at: z.string(),
reading_time_minutes: z.number(),
tag_list: z.array(z.string()),
tags: z.string(),
user: UserSchema
})
export const ArticlesSchema = z.array(ArticleSchema);
Now let's implement this schema on our data fetcher:
// App.tsx
import { useQuery } from "@tanstack/react-query";
import { Article } from "./types/article";
import { z } from "zod";
import { ArticleSchema, ArticlesSchema } from "./schemas/article";
function App() {
const { VITE_BASE_URL } = import.meta.env;
const getArticles = () =>
fetch(VITE_BASE_URL)
.then((res) => res.json())
.then((data) => ArticlesSchema.parse(data));
const { data, isLoading, isFetching, error, isError } = useQuery({
queryKey: ["articles"], // a key that will help to retrieve data
queryFn: getArticles, // the function that will be executed to actually fetch the data
});
if (isLoading) {
return <p>Data is loading...</p>;
}
if (isFetching) {
return <p>Data is fetching...</p>;
}
if (error) {
return <p>There was an error when fetching your data.</p>;
}
return (
<ul>
{data?.map((article) => {
return <li key={article.id}>{article.title}</li>;
})}
</ul>
);
}
export default App;
Let's try to run our application and ...
Uh oh! We've got an error, but what a beautiful and accurate error! You can see that with the help of Zod, we can easily know which data is faulty.
Because we don't like to see these red lines, let's correct our article schema:
// schemas/article.ts
export const ArticleSchema =
z.object({
type_of: z.string(),
id: z.number(),
title: z.string(),
description: z.string(),
readable_publish_date: z.string(),
slug: z.string(),
path: z.string(),
url: z.string(),
comments_count: z.number(),
public_reactions_count: z.number(),
published_timestamp: z.string(),
collection_id: z.nullable(z.string()),
positive_reactions_count: z.number(),
cover_image: z.string(),
social_image: z.string(),
canonical_url: z.string(),
created_at: z.string(),
edited_at: z.nullable(z.string()),
published_at: z.string(),
last_comment_at: z.string(),
reading_time_minutes: z.number(),
tag_list: z.array(z.string()),
tags: z.string(),
user: UserSchema
})
By making certain keys nullable, we got rid of those red lines. Zod helped us to validate data efficiently.
Conclusion
React Query, TypeScript, and Zod are powerful tools that can greatly improve the efficiency and reliability of web application development.
React Query handles data fetching and caching where Zod is handling data validation.
Together, these tools form a robust framework for building modern web applications. As such, you should consider incorporating them into your development workflows.
That's it folks! See you next time! π
Top comments (2)
What is the difference between data is loading and data is fetching?
Is the there an equivalent of React Query for VueJs ?
For the difference between loading and fetching, i suggest you to log them in your side. That way you can experiment it directly. π
React query is apparently available for Solid, React, Vue and Svelte.