DEV Community

Cover image for Calling your APIs with hooks in react 🧑🏽‍💻
Jorge Exequiel Gómez
Jorge Exequiel Gómez

Posted on • Updated on

Calling your APIs with hooks in react 🧑🏽‍💻

Hi all! This is my first post, and I want to bring an interesting topic:

  • How do we call an API from our react project?
  • What is the best approach?

Of course, there is no silver bullet, and it depends on the project you are working on. Today I will share a few optimizations you can make in your calls and could be a trigger for new ideas.

The problem

During my career, I worked on different projects, and I've found things like this:

Example 1

export const MyComponent: React.FC = () => {
    const [dogs, setDogs] = useState();

    useEffect(() => {
        fetch('/api/v1/dogs')
            .then(r => r.json())
            .then(json => setDogs(json));
    });

    return <DogsList dogs={dogs} />;
}

export const MyComponent2: React.FC = () => {
    const [cats, setCats] = useState();

    useEffect(() => {
        fetch('/api/v1/cats')
            .then(r => r.json())
            .then(json => setData(json));
    });

    return <CatsList cats={cats} />;
}
Enter fullscreen mode Exit fullscreen mode

or this:

Example 2

const MyComponent: React.FC = () => {
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState();
    const [dogs, setDogs] = useState();

    useEffect(() => {
        fetch('/api/v1/dogs')
            .then(r => r.json())
            .then(json => setDogs(json))
            .catch(e => setError(e))
            .finally(() => setLoading(false));
    });

    if (loading) {
        return <div>Loading dogs</div>;
    }

    return <DogsList dogs={dogs} />;
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the code starts to duplicate, and we are putting the communication logic inside our component. And it gets even worse if we want to add more logic, i.e. set the state only if the component is mounted.

For some reason, people sometimes forget we can create a simple hook to handle all these scenarios and keep our code cleaner.

Note: Of course, there are libraries to tackle this, but just for the purpose of this article I prefer we implement it by ourselves

1: A simple approach

Let's start with a small implementation of a new hook to retrieve the data from an API. As we are good at naming things here, let's call it useApi:

function useApi(url: string) {
    const [data, setData] = useState();

    useEffect(() => {
        fetch(url)
            .then(r => r.json())
            .then(json => setData(json))
    }, [url]) // Remember your dependencies

    return data;
}
Enter fullscreen mode Exit fullscreen mode

Only with this simple hook, we could rewrite the first example to this:

export const MyComponent: React.FC = () => {
    const dogs = useApi('/api/v1/dogs');

    return <DogsList dogs={dogs} />;
}

export const MyComponent2: React.FC = () => {
    const cats = useApi('/api/v1/cats');

    return <CatsList cats={cats} />;
}
Enter fullscreen mode Exit fullscreen mode

Look how clean this is, my component does not care about how we call this API, if we are using fetch or axios, we just know the data will be there.

A small step for improvement

Let's iterate this a little bit more, we've forgotten some power we have here... We have typescript! And we are not using the most important feature it provides to us: Types.

function useApi<T>(url: string): T | undefined {
    const [data, setData] = useState<T>();

    useEffect(() => {
        fetch(url)
            .then(r => r.json())
            .then(json => setData(json))
    }, [url]) // Remember your dependencies

    return data;
}
Enter fullscreen mode Exit fullscreen mode

Now, in our components, we are going to have static validation with the proper types

export const MyComponent: React.FC = () => {
    const dogs = useApi<Dog>('/api/v1/dogs');

    return <DogsList dogs={dogs} />;
}

export const MyComponent2: React.FC = () => {
    const cats = useApi<Cat>('/api/v1/cats');

    return <CatsList cats={cats} />;
}
Enter fullscreen mode Exit fullscreen mode

With this first approach we have:

  • Removed duplicated code in both components
  • Separated communication logic from component logic
  • Added static validation for our models

2: Managing the state of the request

Now that we are happy with our useApi implementation, we want to display to the user if we are waiting for the data, or if there is an error fetching our resources.

Adding a few states and returning a tuple we can achieve the following:

function useApi<T>(url: string): [T | undefined, boolean, Error | undefined] {
    const [data, setData] = useState<T>();
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<Error>();

    useEffect(() => {
        setLoading(true);
        fetch(url)
            .then(r => r.json())
            .then(json => setData(json))
            .catch(e => setError(e))
            .finally(() => setLoading(false));
    }, [url]) // Remember your dependencies

    return [data, loading, error];
}
Enter fullscreen mode Exit fullscreen mode

And then in our component:

const MyComponent: React.FC = () => {
    const [dogs, loading, error] = useApi('/api/v1/dogs');

    if (loading) {
        return <div>Loading dogs</div>;
    }

    if (error) {
        return <div>Oops!</div>;
    }

    return <DogsList dogs={dogs} />;
}
Enter fullscreen mode Exit fullscreen mode

3 (Update): Improving our communication

Now that we have finished with the abstraction layer there is another issue. Calling our APIs only using useEffect could lead to duplicated calls (thanks for the comments 😃), or if you want to add a cache or auto refresh, would be hard to integrate in the designs above.

Since we have our own layer with our own hook, we have the flexibility to change it using any library we want, and we don't need to refactor the entire codebase to change it.

For example if we want to use react-query:

import { useQuery } from 'react-query'

const fetcher = (url) => () => fetch(url).then(r => r.json());

function useApi<T>(url: string): [T | undefined, boolean, Error | undefined] {
    const { data, isLoading, isError } = useQuery(url, fetcher(url));

    return [data, isLoading, error];
}
Enter fullscreen mode Exit fullscreen mode

And we don't need to touch our component.

Remember: This is for example purposes only, if you want to use react-query or another library read the documentation of the proper usage.

Conclusion

With this approach, we were able to create a custom hook that allow us to eventually do any modification we want without the need of refactoring all of our code. We don't have repeated code across our components, and we are handling different statuses from the request properly.

Adding another library to make the requests would be straightforward, and even we can extract the fetcher: (url: string) => Promise<T> to allow the users to decide which library to use.

Thank you very much to read until the end, I hope this helped you a little bit 😃. Feedback is always appreciated.

Top comments (1)

Collapse
 
ecyrbe profile image
ecyrbe • Edited

Nowadays, i would recommend replacing all custom fetching hooks with react-query or similar data fetching hook and not even try to do this yourself.

Indeed, in react 18, using useEffect to fetch data can sometimes ( always in dev builds) mean double API fetch calls as your components can now be unmounted and remounted with new react concurrent mode.

React-query is react 18 compliant and will give more benefits than any custom Hook you'll cook.