Skip to main content

User list example

User lists

The next example fetches an array of user objects from the REST API and store it to React state. The REST API to be used is Reqres.in fake API, and the following URL returns a list of fake users (https://reqres.in/api/users).

Persons example

Let's define a type for the user object:

type User = {
id: number;
email: string;
first_name: string;
last_name: string;
avatar: string;
}

We need an array to store a list of users; therefore, we create a state called users and initialize that to an empty array. Then, we use the useEffect hook to send a request once after the first render.

const [users, setUsers] = useState<User[]>([]);

useEffect(() => {
fetch('https://reqres.in/api/users')
.then(response => {
if (!response.ok)
throw new Error("Error in fetch: " + response.statusText);

return response.json();
})
.then(responseData => setUsers(responseData.data))
.catch(err => console.error(err))
}, [])

Now, if you open the developer console in your browser and navigate to the Network tab, you can see the requests made by the component. You will find the users request triggered by our component. The response payload is also displayed in the console. You might notice that the users request is executed twice. This behavior is due to React.StrictMode that is enabled in the main.tsx file. React.StrictMode invokes certain lifecycle methods twice during development to help identify potential issues. This behavior is specific to the development environment and will not occur in the production build of your application.

Network console

The return statement looks the following. Each user object includes a unique id, which is used as the key for each table row:

return (
<>
<h2>Users</h2>
<table>
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{users.map((user: User) => (
<tr key={user.id}>
<td>{user.first_name}</td>
<td>{user.last_name}</td>
<td>{user.email}</td>
</tr>
))}
</tbody>
</table>
</>
)

Now, the list should look like the following screenshot:

Typing fetch & Refactoring

We can define types to represent the structure of the API response. Next, we will learn how to perform type-safe fetching using the same Reqres.in REST API as before.

The response data looks the following:

{
"page": 1,
"per_page": 6,
"total": 12,
"total_pages": 2,
"data": [
{
"id": 1,
"email": "george.bluth@reqres.in",
"first_name": "George",
"last_name": "Bluth",
"avatar": "https://reqres.in/img/faces/1-image.jpg"
},
{
"id": 2,
"email": "janet.weaver@reqres.in",
"first_name": "Janet",
"last_name": "Weaver",
"avatar": "https://reqres.in/img/faces/2-image.jpg"
},
{
"id": 3,
"email": "emma.wong@reqres.in",
"first_name": "Emma",
"last_name": "Wong",
"avatar": "https://reqres.in/img/faces/3-image.jpg"
},
],
"support": {
"url": "https://contentcaddy.io?utm_source=reqres&utm_medium=json&utm_campaign=referral",
"text": "Tired of writing endless social media content? Let Content Caddy generate it for you."
}
}

We need to define a type to represent the structure of the REST API response:

export type ApiResponse = {
page: number;
per_page: number;
total: number;
total_pages: number;
data: User[];
support: {
url: string;
text: string;
}
}

We can define the responseData with the ApiResponse type to ensure type safety:

useEffect(() => {
fetch("https://reqres.in/api/users")
.then((response) => {
if (!response.ok) throw new Error(`Error in fetch: ${response.status}`)

return response.json()
})
.then((responseData: ApiResponse) => setUsers(responseData.data))
.catch((err) => console.error(err))
}, [])

Additionally, we can create own module to handle REST API functions. This approach is beneficial when the same request needs to be made in multiple components, as it avoids code duplication. It also simplifies REST API testing which is commonly made by mocking.

To improve reusability and maintainability, define the types in a separate module and export them for use in other parts of the application. you can also create a dedicated module (e.g., userApi.ts) to handle REST API interactions. In this approach, API logic is easier to manage and test.

For example, you can define and export the User and ApiResponse types in a types.ts file:

types.ts
export type User = {
id: number;
email: string;
first_name: string;
last_name: string;
avatar: string;
}

export type ApiResponse = {
page: number;
per_page: number;
total: number;
total_pages: number;
data: User[];
support: {
url: string;
text: string;
};
};

Then, in the userApi.ts module, handle the API request logic:

import { ApiResponse } from "./types";

export const fetchUsers = (): Promise<ApiResponse> => {
return fetch("https://reqres.in/api/users").then((response) => {
if (!response.ok) {
throw new Error(`Error in fetch: ${response.status}`);
}
return response.json();
});
}

The fetchUsers function returns a Promise. We can use generics to define value that Promise resolves and in our case it is ApiResponse. That's why function's return type is Promise<ApiResponse>.

When the Promise resolves, the resolved value will match the structure defined by the ApiResponse type. If you try to access properties on the resolved value that are not part of ApiResponse, TypeScript will throw a compile-time error.

Now, developers know exactly what to expect when the Promise resolves. You can also get autocompletion support for the ApiResponse type when type is defined.

Then, we can use fetchUsers function in UserList component.

useEffect(() => {
fetchUsers()
.then(responseData => setUsers(responseData.data))
.catch(err => console.error(err))
}, [])

Now, you can see the TypeScript error if you try to access properties that doesn't exist in ApiResponse. You will also get autocompletion as shown in the following image:

Autocompletion example

info

You can also create custom hook to perform REST API call. Read more about custom hooks in https://react.dev/learn/reusing-logic-with-custom-hooks.