The Best Way to Handle Promises in React

marileon

Omari

Posted on February 6, 2024

The Best Way to Handle Promises in React

Promises are a great feature in JavaScript, used for data fetching or any asynchronous code. At first, it can be a struggle to figure out the best way to integrate Promises into the React ecosystem, so in this article we’ll go through a few options of how to do exactly that.

What is a Promise?

Promises allow you to perform asynchronous operations in JavaScript. To construct a Promise from scratch, you can use the Promise constructor. This takes a function which takes two parameters: “resolve”, a function to call when the operation completes, and “reject”, a function to call if the operation fails. You then have to call one of these functions when your operation completes.

Promises are a way of performing asynchronous operations in JavaScript. They’re a way of saying “I don’t have the value of this now, but I will at some point in the future”.

You can construct a promise from scratch using the Promise constructor. It takes two functions as parameters: “resolve”, a function to call when the operation completes successfully, and “reject”, a function to call if the operation fails. You then have to call one of these functions when your operation completes, called “settling” a promise.

JavaScript includes two ways of getting the result of a promise. Let’s take a look at both.

.then()

The first is “.then()”, a method on Promises which accepts a callback function to run when the promise resolves. Here’s what that looks like:

const myPromise = new Promise((resolve) => {
    setTimeout(() => {
        resolve('follow @marile0n');
    }, 1000); //1000ms = 1s
});
myPromise.then((val) => {
    console.log(val);
});
Enter fullscreen mode Exit fullscreen mode

.then() returns another promise, which lets you chain together actions:

const healthyItems = ['apple', 'banana', 'carrot'];
function fetchShoppingBasket(): Promise<string[]> {
    //...
}
function countHealthyItems() {
    fetchShoppingBasket()
        .then((items) => items.filter((item) => healthyItems.includes(item)))
        .then((healthyItems) => healthyItems.length);
}
Enter fullscreen mode Exit fullscreen mode

This works even if the next function you call in .then() isn’t asynchronous.

There are also two other related methods, .catch() and .finally(). .catch() is used for catching errors, and .finally() runs after a promise is settled, i.e. whether it completes successfully, or throws an error:

myPromise
    .then(() => console.log('Ill run first'))
    .then(() => console.log('Ill run second'))
    .then(() => {
        throw new Error('Whoops!');
    })
    .catch((e) => console.log('Ill run after an error:', e))
    .finally(() => console.log('Ill run last'));
Enter fullscreen mode Exit fullscreen mode

Async/Await

The other way of handling promises in React is the async/await syntax. You can “await” a function to synchronously wait for the promise to settle:

const myPromise = new Promise((resolve) => {
    setTimeout(() => {
        resolve('follow @marile0n');
    }, 1000); //1000ms = 1s
});
const result = await myPromise;
console.log(result);
Enter fullscreen mode Exit fullscreen mode

This lets you use a regular try…catch statement to handle errors. Here’s the above promise, converted to async/await

try {
    await myPromise;
    console.log('Ill run first');
    console.log('Ill run second');
    throw new Error('Whoops!');
} catch (e) {
    console.log('Ill run after an error:', e);
} finally {
    console.log('Ill run last');
}
Enter fullscreen mode Exit fullscreen mode

If you use await in a function, it must be marked as asynchronous with the “async” keyword:

async function myAsyncFunction() {
    //...
}
const myLambdaAsyncFunction = async () => {
    //...
};
Enter fullscreen mode Exit fullscreen mode

Now, onto the React part.

The Plain Way

You can handle a Promise in React using useEffect to call the Promise, and a useState to store the result. It’s also useful to set up some other values to track the state of the asynchronous action.

We’ll start off with our Promise. We’ll be using the handy — A great API for retrieving pictures of cats. Here’s the code for retrieving a cat:

type CatResponse = { _id: string };
async function getCat() {
    return axios
        .get<CatResponse>('https://cataas.com/cat?json=true')
        .then((res) => {
            console.log(res.data);
            return 'https://cataas.com/cat/' + res.data._id;
        });
}
Enter fullscreen mode Exit fullscreen mode

Now let’s build that together in a React component. We’ll set up some state hooks:

const [cat, setCat] = useState('');
const [status, setStatus] = useState<'pending' | 'success' | 'error'>(
    'pending'
);
const [error, setError] = useState<Error>();
Enter fullscreen mode Exit fullscreen mode

Three state values — one to track the returned cat, one to track the state of the promise, and one to store an error if we get one.

Then we have our useEffect:

useEffect(() => {
    setStatus('pending');
    getCat()
        .then((cat) => {
            setCat(cat);
            setStatus('success');
        })
        .catch((e) => {
            console.log(e);
            setStatus('error');
            setError(e);
        });
}, []);
Enter fullscreen mode Exit fullscreen mode

To use the value of our promise, we can use a useEffect() hook with an empty dependency array. The function will get called the first time the component mounts, which changes our state and performs our API call. Then when the API call is done, our state values get updated, or if there’s an error, that gets stored.

Or we can rewrite this using async/await:

useEffect(() => {
    setStatus('pending');
    try {
        const cat = await getCat(); //'await' expressions are only allowed within async functions and at the top levels of modules.
        setCat(cat);
        setStatus('success');
    } catch (e) {
        console.log(e);
        setStatus('error');
        setError(e as Error);
    }
}, []);
Enter fullscreen mode Exit fullscreen mode

Oh wait, but since we’re using await, we need to mark our function as async:

//Effect callbacks are synchronous to prevent race conditions.
useEffect(async () => {
    setStatus('pending');
    try {
        const cat = await getCat();
        setCat(cat);
        setStatus('success');
    } catch (e) {
        console.log(e);
        setStatus('error');
        setError(e as Error);
    }
}, []);
Enter fullscreen mode Exit fullscreen mode

Now we run into another error — the useEffect function cannot be asynchronous.

How do we solve this? Move the code to an async function, and then call that function from your useEffect:

useEffect(() => {
    async function fetchCat() {
        setStatus('pending');
        try {
            const cat = await getCat();
            //'await' expressions are only allowed within async functions and at the top levels of modules.
            setCat(cat);
            setStatus('success');
        } catch (e) {
            console.log(e);
            setStatus('error');
            setError(e as Error);
        }
    }
    fetchCat();
}, []);
Enter fullscreen mode Exit fullscreen mode

Then we can put this together with some JSX to display the results:

function Cat() {
    const [cat, setCat] = useState('');
    const [status, setStatus] = useState<'pending' | 'success' | 'error'>(
        'pending'
    );
    const [error, setError] = useState<Error>();
    useEffect(() => {
        async function fetchCat() {
            setStatus('pending');
            try {
                const cat = await getCat(); //'await' expressions are only allowed within async functions and at the top levels of modules.
                setCat(cat);
                setStatus('success');
            } catch (e) {
                console.log(e);
                setStatus('error');
                setError(e as Error);
            }
        }
        fetchCat();
    }, []);
    if (status === 'pending') return <h1>Loading...</h1>;
    if (status === 'error') return <h1>Error! {error?.message}</h1>;
    return (
        <div className="relative h-full w-96">
            {' '}
            <Image
                src={cat}
                alt="Cat"
                fill
                className="h-full w-full object-contain"
            />{' '}
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

And here’s what it all looks like:

So pretty straightforward, but a little verbose, even with just the basic functionality we’ve implemented here.

The Better Way

If you look online, you’ll see a lot of negative sentiment around using useEffect to handle promises. The main reason why is that it’s preferable to use existing libraries, rather than re-inventing the wheel. The most popular library for handling asynchronous promises in React is TanStack Query (formerly known as React Query). Let’s take a look at it by recreating our previous example with TanStack Query.

First you’ll need to install TanStack Query:

npm install @tanstack/react-query
yarn add @tanstack/react-query 
pnpm add @tanstack/react-query
Enter fullscreen mode Exit fullscreen mode

Then we need to set up a QueryClient and a provider for our components to be able to access the client. I’m using the app directory in Next.js, so here’s how I’ve done it:

//Proiders.tsx
('use client');
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { WithChildrenProps } from '@/types';
const queryClient = new QueryClient();
export function Providers({ children }: WithChildrenProps) {
    return (
        <QueryClientProvider client={queryClient}>
            {' '}
            {children}{' '}
        </QueryClientProvider>
    );
}

//layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { Providers } from './providers';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
    title: 'Create Next App',
    description: 'Generated by create next app',
};
export default function RootLayout({
    children,
}: Readonly<{ children: React.ReactNode }>) {
    return (
        <html lang="en">
            {' '}
            <body className={inter.className}>
                {' '}
                <Providers>{children}</Providers>{' '}
            </body>{' '}
        </html>
    );
}
Enter fullscreen mode Exit fullscreen mode

Then, essentially all the extra logic we wrote before can be swapped out for React Query:

function TanCatQuery() {
    const {
        data: cat,
        isPending,
        error,
    } = useQuery({ queryKey: ['cat'], queryFn: () => getCat() });
    if (isPending) return <h1>Loading...</h1>;
    if (error) return <h1>Error! {error.message}</h1>;
    return (
        <div className="relative h-full w-96">
            {' '}
            <Image
                src={cat}
                alt="Cat"
                fill
                className="h-full w-full object-contain"
            />{' '}
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

The useQuery hook accepts an asynchronous function to call, as well as a key to be used when caching queries, and in return we get all the logic we previously had. React Query also has a lot of other benefits such as built-in caching, reduping requests, automatic retrying, etc.

Conclusion

So which one should you use? If you’re performing your promises outside of a React component, you can skip React Query, otherwise the choice is yours. Personally, I tend to reach for React Query straight away, as it greatly simplifies asynchronous code in React, but it’s all up to you — pick the best fit for your app. Check out the full code here and thanks for reading!

Originally published at https://www.omarileon.me.

💖 💪 🙅 🚩
marileon
Omari

Posted on February 6, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related