Last updated: Apr 7, 2024
Reading time·5 min

The warning "useEffect must not return anything besides a function, which is
used for clean-up." occurs when you return a value that is not a function from
your useEffect hook.
To solve the error, define an async function within your useEffect hook
and call it.

Here is the complete stack trace.
Warning: useEffect must not return anything besides a function, which is used for clean-up. It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately:
Here is a code sample that causes the warning.
import {useState, useEffect} from 'react'; function App() { const [users, setUsers] = useState([]); // ⛔️ Effect callbacks are synchronous to prevent race conditions. Put the async function inside: // ⛔️ defining the function as async causes the issue useEffect(async () => { const response = await fetch('https://randomuser.me/api/'); const data = await response.json(); setUsers(data.results); }, []); console.log(users); return ( <div> {users.map(user => ( <div key={user.id.value}> <h2> Name: {user.name.first} {user.name.last} </h2> </div> ))} </div> ); } export default App;

Notice that we defined the function we passed to the useEffect hook as async.
async return a Promise object.This is an issue because the useEffect hook should only return a function that
is used for clean-up.
The clean-up function is called when the component unmounts and is often used to remove event listeners and carry out other tasks that allow you to avoid memory leaks.
Make sure you don't have a return statement that returns anything other than a
clean-up function in your useEffect hook (e.g. a Promise).
async function inside your useEffect hookTo solve the error, write the async function inside your useEffect hook and
call it.
import {useState, useEffect} from 'react'; function App() { const [users, setUsers] = useState([]); // 👇️ Notice that this function is no longer async useEffect(() => { // ✅ Define the async function here async function getUsers() { const response = await fetch('https://randomuser.me/api/'); const data = await response.json(); setUsers(data.results); } // 👇️ Call the function here getUsers(); }, []); console.log(users); return ( <div> {users.map(user => ( <div key={user.id.value}> <h2> Name: {user.name.first} {user.name.last} </h2> </div> ))} </div> ); } export default App;
Notice that the function we passed to the useEffect hook is no longer async.
async functions return a Promise even if you don't explicitly use a return statement.Instead, we defined the async function inside of the useEffect hook and
called it.
When the component mounts, the useEffect hook runs and the getUsers()
function is invoked.
The function fetches data from a remote API and updates the state.
Here is a complete example that sets a loading spinner and error state.
import {useState, useEffect} from 'react'; function App() { const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(false); const [err, setErr] = useState(''); useEffect(() => { async function getUsers() { setIsLoading(true); try { const response = await fetch( 'https://randomuser.me/api/', ); const data = await response.json(); console.log('data is: ', JSON.stringify(data, null, 4)); setUsers(data.results); } catch (err) { setErr(err.message); } finally { setIsLoading(false); } } getUsers(); }, []); return ( <div> {err && <h2>{err}</h2>} {isLoading && <h2>Loading...</h2>} {users.map(user => ( <div key={user.id.value}> <h2> Name: {user.name.first} {user.name.last} </h2> </div> ))} </div> ); } export default App;

We added 2 more state variables - isLoading and err.
The isLoading boolean is used to track the loading state and the err state
variable tracks the error state.
Once we start fetching data, we immediately set the loading state to true.
async function getUsers() { setIsLoading(true); try { const response = await fetch( 'https://randomuser.me/api/', ); const data = await response.json(); console.log('data is: ', JSON.stringify(data, null, 4)); setUsers(data.results); } catch (err) { setErr(err.message); } finally { setIsLoading(false); } }
We added a try/catch statement to handle a potential error.
If the fetch request succeeds, the users state variable gets set to the
users from the remote API.
If an error occurs, the catch block runs where we set the error state.
The finally block is run regardless if the HTTP request is successful or not.
In the finally block, we simply set the loading state to false.
useEffect hookAs the warning states, "useEffect must not return anything besides a function, which is used for clean-up".
Let's look at an example that returns a clean-up function from the useEffect
hook to fully understand how it works.
As previously noted, the clean-up function is most commonly used to remove event listeners, track whether the component is mounted and in general to help us avoid memory leaks.
The following example uses the clean-up function to set an isMounted variable
to false.
import {useState, useEffect} from 'react'; function App() { const [users, setUsers] = useState([]); useEffect(() => { // 👇️ Initialize `isMounted` to `true` let isMounted = true; async function getUsers() { const response = await fetch('https://randomuser.me/api/'); const data = await response.json(); console.log('data is: ', JSON.stringify(data, null, 4)); // 👇️ Only update the state if the component is mounted if (isMounted) { setUsers(data.results); } } getUsers(); return () => { // 👇️ When the component unmounts, set isMounted to false isMounted = false; }; }, []); return ( <div> {users.map(user => ( <div key={user.id.value}> <h2> Name: {user.name.first} {user.name.last} </h2> </div> ))} </div> ); } export default App;
We initialized the isMounted variable to true inside our useEffect hook.
// 👇️ Initialize `isMounted` to `true` let isMounted = true;
We'll use the variable to track whether the component is mounted or not.
This is useful because if you try to update the state of an unmounted component you'd get the "Can't perform a react state update on an unmounted component" warning.
The getUsers function uses the value of the isMounted variable to determine
whether it should update the state.
async function getUsers() { const response = await fetch('https://randomuser.me/api/'); const data = await response.json(); console.log('data is: ', JSON.stringify(data, null, 4)); // 👇️ Only update the state if the component is mounted if (isMounted) { setUsers(data.results); } }
If the component is no longer mounted, we don't want to attempt to update the state because it is pointless and a sign of a memory leak.
The function we returned from the useEffect hook is run when the component
unmounts.
return () => { // 👇️ When the component unmounts, set isMounted to false isMounted = false; };
The function simply sets the isMounted variable to false.
If the getUsers function runs after the component is unmounted, the condition
in the if statement is not met and we don't attempt to update the state.
I've also written an article on how to fetch data on button click in React.js.
The article shows examples that use the built-in fetch and the third-party
axios modules.
I've also written an article on
how to run a React hook when a component unmounts
with an example that uses the removeEventListener method.
To resolve the "useEffect must not return anything besides a function, which is
used for clean-up." warning, make sure the function you've passed to the
useEffect hook is not marked as async.
All async functions return a promise, even if you don't use an explicit
return statement.
Instead, define the async function inside the useEffect hook and call the
async function.
You can learn more about the related topics by checking out the following tutorials: