How to make asynchronous requests to your API in React
Tags: react, api, programming, javascript
Published: 2021-11-07 02:51:54

This is a common problem that beginner React developers face when working on a new project. I will show here what you are doing and a method you can use to have a better and cleaner code (with tests!).

Let's suppose that we are developing a new blog application that will render a simple list of posts based on the response of our API. Usually what we have is this:

```tsx
import { useEffect, useState } from 'react';

import axios from 'axios';

import { Post } from '../../types/post';
import Pagination from '../Pagination/Pagination';
import PostCard from '../PostCard/PostCard';

const DirBlogPosts: React.FC = () => {
  const [page, setPage] = useState<number>(1);
  const [posts, setPosts] = useState<Array<Post>>([]);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [isError, setIsError] = useState<boolean>(false);

  useEffect(() => {
    (async () => {
      try {
        setIsLoading(true);
        const { data } = await axios.get<Array<Post>>('https://example.com/posts', {
          params: { page },
        });
        setPosts(data);
      } catch (error) {
        setIsError(true);
      } finally {
        setIsLoading(false);
      }
    })();
  }, [page]);

  if (isLoading) {
    return <p>Loading posts...</p>;
  }

  if (isError) {
    return <p>There was an error trying to load the posts.</p>;
  }

  return (
    <div>
      {posts.map((post) => (
        <PostCard post={post} />
      ))}
      <Pagination page={page} onChangePage={setPage} />
    </div>
  );
};

export default DirBlogPosts;
```

Here we have the states `page`, `posts`, `isLoading` and `isError`. These states are updated when the component renders for the first time, or whenever the `page` is changed.

Can you see the problem here?

1. We have all the fetching logic inside our component;
2. We need to control many states manually;
3. It's hard to create automated tests.

But we can try to follow a different approach and create a cleaner code.

## Build your service

First of all, taking advantage of Typescript's features, let's define what is a post:

```tsx
// src/types/post.ts
export type Post = {
  id: number;
  title: string;
  imageUrl: string;
  content: string;
};
```

The post is basically an object with `id`, `title` , `imageUrl` and `content`.

Now we can create the definition of our "list posts service":

```tsx
// src/services/definitions/list-posts-service.ts
import { Post } from '../../types/post';

export interface ListPostsService {
  list(params: ListPostsService.Params): Promise<ListPostsService.Result>;
}

export namespace ListPostsService {
  export type Params = {
    page?: number;
  };

  export type Result = Array<Post>;
}
```

Here we define that the "list post service" implementation should have a method called `list`, that will receive the defined params and return the defined result.

Why have we created an interface for that?

The answer is simple: our component will receive this service and execute it. The component doesn't even need to know if you will be using Axios or Fetch. Let's say your component will be agnostic. Sometime later you may need to change the Axios to Fetch, or even use Redux.

So, let's build our Axios service implementation:

```tsx
// src/services/implementation/axios-list-posts-service.ts
import { AxiosInstance } from 'axios';

import { Post } from '../../types/post';
import { ListPostsService } from '../definitions/list-posts-service';

export default class AxiosListPostsService implements ListPostsService {
  constructor(private readonly axiosInstance: AxiosInstance) {}

  async list(params: ListPostsService.Params): Promise<ListPostsService.Result> {
    const { data } = await this.axiosInstance.get<Array<Post>>('/posts', {
      params: { page: params.page },
    });

    return data;
  }
}
```

This is our implementation using Axios. We need the Axios instance in the constructor, and in the method `list` we make the request to our endpoint `/posts`.

As we are already working on this service, let's also create a mocked version to use on the tests:

```tsx
import faker from 'faker';
import lodash from 'lodash';

import { ListPostsService } from './list-posts-service';

export const mockListPostsServicesResult = (): ListPostsService.Result => {
  return lodash.range(10).map((id) => ({
    id,
    title: faker.lorem.words(),
    content: faker.lorem.paragraphs(),
    imageUrl: faker.internet.url(),
  }));
};

export class ListPostsServiceSpy implements ListPostsService {
  params: ListPostsService.Params;

  result: ListPostsService.Result = mockListPostsServicesResult();

  async list(params: ListPostsService.Params): Promise<ListPostsService.Result> {
    this.params = params;

    return this.result;
  }
}
```

We just need to store in the class the params and a mocked result to test using Jest later. For the mocked data, I like to use the Faker.js library.

### Build your clean component

To manage all the loading and error states that we might need, I like to use the library React Query. You can read the documentation to get every detail on how to include it in your project. Basically you only need to add a custom provider wrapping your app, because the React Query also manages caches for the requests.

```tsx
import { useState } from 'react';

import { useQuery } from 'react-query';

import { ListPostsService } from '../../services/definitions/list-posts-service';
import Pagination from '../Pagination/Pagination';
import PostCard from '../PostCard/PostCard';

type CleanBlogPostsProps = {
  listPostsService: ListPostsService;
};

const CleanBlogPosts: React.FC<CleanBlogPostsProps> = ({ listPostsService }) => {
  const [page, setPage] = useState<number>(1);
  const {
    data: posts,
    isLoading,
    isError,
  } = useQuery(['posts', page], () => listPostsService.list({ page }), { initialData: [] });

  if (isLoading) {
    return <p data-testid="loading-posts">Loading posts...</p>;
  }

  if (isError) {
    return <p data-testid="loading-posts-error">There was an error trying to load the posts.</p>;
  }

  return (
    <div>
      {posts!.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
      <Pagination page={page} onChangePage={setPage} />
    </div>
  );
};

export default CleanBlogPosts;
```

Do you see now how much cleaner it is? As a result of `useQuery` we have all the states that we need: our data, the loading and the error state. You don't need to use the `useEffect` for that anymore. The first parameter in `useQuery` can be a string or an array. When I include the `page` in this array, it means that the query will refetch using this new value (whenever the page changes, like in the `useEffect`).

I also added some `data-testid` that will be used for testing. So, let's build it!

### Build your component test

Our component required the `listPostsService`, so let's use the `ListPostsServiceSpy` that we created before. Using this we won't make a real HTTP request, because it's a "fake service".

```tsx
import { render, screen } from '@testing-library/react';
import reactQuery, { UseQueryResult } from 'react-query';

import { ListPostsServiceSpy } from '../../services/definitions/mock-list-posts-service';
import CleanBlogPosts from './CleanBlogPosts';

type SutTypes = {
  listPostsServiceSpy: ListPostsServiceSpy;
};

const makeSut = (): SutTypes => {
  const listPostsServiceSpy = new ListPostsServiceSpy();

  return {
    listPostsServiceSpy,
  };
};

jest.mock('react-query', () => ({
  useQuery: () => {
    return {
      data: [],
      isLoading: false,
      isError: false,
    };
  },
}));

describe('CleanBlogPosts', () => {
  it('should show loading state', async () => {
    const { listPostsServiceSpy } = makeSut();

    jest.spyOn(reactQuery, 'useQuery').mockReturnValueOnce({
      data: listPostsServiceSpy.result,
      isLoading: true,
      isError: false,
    } as any);

    render(<CleanBlogPosts listPostsService={listPostsServiceSpy} />);

    expect(screen.getByTestId('loading-posts')).toBeInTheDocument();
  });

  it('should show error state', async () => {
    const { listPostsServiceSpy } = makeSut();

    jest.spyOn(reactQuery, 'useQuery').mockReturnValueOnce({
      data: listPostsServiceSpy.result,
      isLoading: false,
      isError: true,
    } as any);

    render(<CleanBlogPosts listPostsService={listPostsServiceSpy} />);

    expect(screen.getByTestId('loading-posts-error')).toBeInTheDocument();
  });

  it('should list the posts', async () => {
    const { listPostsServiceSpy } = makeSut();

    jest.spyOn(reactQuery, 'useQuery').mockReturnValueOnce({
      data: listPostsServiceSpy.result,
      isLoading: false,
      isError: false,
    } as UseQueryResult);

    render(<CleanBlogPosts listPostsService={listPostsServiceSpy} />);

    const posts = await screen.findAllByTestId('post-card');

    expect(posts).toHaveLength(listPostsServiceSpy.result.length);
  });
});
```

We added 3 tests:

- loading state: check if our `useQuery` returns the state `isLoading: true`, we will render the loading component.
- error state: check if our `useQuery` returns the state `isError: true`, we will render the error component.
- success: check if our `useQuery` returns the state `data`, we will render what we want (the list of posts cards). I also checked if we rendered the same amount of posts returned by our service.

### Conclusion

This is not "the best solution for your API". Each case might need a different solution. But I hope this helps you to see the alternatives for developing a better code.

Another alternative is to create a custom hook called `useListPosts()` that will return the same state as `useQuery`, but you also decouple the React Query from your component and use your own implementation in order to create more tests.

Unfortunately, it is not common to see automated tests in front-end code, it is rarely taught in courses. Now open your VSCode and try it :)