The unopinionated nature of React is a two-edged sword:
This article is the second part of a series about software architecture and React apps where we take a code base with lots of bad practices and refactor it step by step.
In this article, we want to focus on separating API related code from the UI components.
Let’s have a look at the bad code example. Here’s the result of the first refactoring step from the previous article: a component that fetches data from two API endpoints and renders the data.
import { useEffect, useState } from "react";import { useParams } from "react-router";import { apiClient } from "@/api/client";import { LoadingSpinner } from "@/components/loading";import { ShoutList } from "@/components/shout-list";import { UserResponse, UserShoutsResponse } from "@/types";import { UserInfo } from "./user-info";export function UserProfile() {const { handle } = useParams<{ handle: string }>();const [user, setUser] = useState<UserResponse>();const [userShouts, setUserShouts] = useState<UserShoutsResponse>();const [hasError, setHasError] = useState(false);useEffect(() => {AapiClientA.get<UserResponse>(`/user/${handle}`).then((response) => setUser(response.data)).catch(() => setHasError(true));AapiClientA.get<UserShoutsResponse>(`/user/${handle}/shouts`).then((response) => setUserShouts(response.data)).catch(() => setHasError(true));}, [handle]);if (hasError) {return <div>An error occurred</div>;}if (!user || !userShouts) {return <LoadingSpinner />;}return (<div className="max-w-2xl w-full mx-auto flex flex-col p-6 gap-6"><UserInfo user={user.data} /><ShoutListusers={[user.data]}shouts={userShouts.data}images={userShouts.included}/></div>);}
It’s simple: API requests and UI code are mixed. A bit of API code here, a bit of UI there.
But in fact, for the UI it shouldn’t matter how it get’s the data:
All of this should be an implementation detail from the perspective of the UI.
Extracting functions that connect to the API to a separate place will greatly help decouple API related code from UI code.
These functions will hide the implementation details like
In order to have a clear separation we will locate these functions in a global API folder.
// src/api/user.tsimport { User, UserShoutsResponse } from "@/types";import { apiClient } from "./client";async function getUser(handle: string) {const response = await apiClient.get<{ data: User }>(`/user/${handle}`);return response.data;}async function getUserShouts(handle: string) {const response = await apiClient.get<UserShoutsResponse>(`/user/${handle}/shouts`);return response.data;}export default { getUser, getUserShouts };
Now we use these fetch functions inside the component.
import UserApi from "@/api/user";...export function UserProfile() {const { handle } = useParams<{ handle: string }>();const [user, setUser] = useState<User>();const [userShouts, setUserShouts] = useState<UserShoutsResponse>();const [hasError, setHasError] = useState(false);useEffect(() => {if (!handle) {return;}UserApi.getUser(handle).then((response) => setUser(response.data)).catch(() => setHasError(true));UserApi.getUserShouts(handle).then((response) => setUserShouts(response)).catch(() => setHasError(true));}, [handle]);...
You’re right. The code changes aren’t huge. We basically moved a bit of code around. The result might initially look like somewhat cleaner component code.
// beforeapiClient.get<UserResponse>(`/user/${handle}`)// afterUserApi.getUser(handle)
But in fact, we started to effectively decouple the UI code from API related functionality.
I’m not getting tired of repeating myself: now the component doesn’t know a lot of API related details like
GET
UserResponse
/user/some-handle
Instead it calls a simple function UserApi.getUser(handle)
that returns a typed result wrapped in a promise.
Additionally, we can reuse these fetch functions in multiple components or for different rendering approaches like client-side or server-side rendering.
Yes, we took a big step forward separating our UI code from the API. We introduced an API layer and removed many implementation details from the UI component.
But we still have considerable coupling between the API layer and our components. For example, we transform the response data inside the components:
Now, this might not be the most impressive example. So let’s look at another one from the same code base:
Here we can see how the response of the feed has an included
field that contains users and images.
Not pretty, but sometimes we have to deal with such APIs.
But why should the component know?
Anyway, we’ll deal with that in the next article.