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);
});
.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);
}
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'));
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);
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');
}
If you use await in a function, it must be marked as asynchronous with the “async” keyword:
async function myAsyncFunction() {
//...
}
const myLambdaAsyncFunction = async () => {
//...
};
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;
});
}
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>();
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);
});
}, []);
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);
}
}, []);
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);
}
}, []);
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();
}, []);
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>
);
}
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
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>
);
}
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>
);
}
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.