React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing
Asked Answered
C

20

541

I was trying the useEffect example something like below:

useEffect(async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}, []);

and I get this warning in my console. But the cleanup is optional for async calls I think. I am not sure why I get this warning. Linking sandbox for examples. https://codesandbox.io/s/24rj871r0p enter image description here

Consultative answered 16/11, 2018 at 6:7 Comment(1)
For those who wonder about the explanation behind this, here's a good write up: devtrium.com/posts/async-functions-useeffect The issue here is that the first argument of useEffect is supposed to be a function that returns either nothing (undefined) or a function (to clean up side effects). But an async function returns a Promise, which can't be called as a function! It's simply not what the useEffect hook expects for its first argument.Iscariot
A
816

For React version <=17

I suggest to look at Dan Abramov (one of the React core maintainers) answer here:

I think you're making it more complicated than it needs to be.

function Example() {
  const [data, dataSet] = useState<any>(null)

  useEffect(() => {
    async function fetchMyAPI() {
      let response = await fetch('api/data')
      response = await response.json()
      dataSet(response)
    }

    fetchMyAPI()
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

Longer term we'll discourage this pattern because it encourages race conditions. Such as — anything could happen between your call starts and ends, and you could have gotten new props. Instead, we'll recommend Suspense for data fetching which will look more like

const response = MyAPIResource.read();

and no effects. But in the meantime you can move the async stuff to a separate function and call it.

You can read more about experimental suspense here.


If you want to use functions outside with eslint.

 function OutsideUsageExample({ userId }) {
  const [data, dataSet] = useState<any>(null)

  const fetchMyAPI = useCallback(async () => {
    let response = await fetch('api/data/' + userId)
    response = await response.json()
    dataSet(response)
  }, [userId]) // if userId changes, useEffect will run again

  useEffect(() => {
    fetchMyAPI()
  }, [fetchMyAPI])

  return (
    <div>
      <div>data: {JSON.stringify(data)}</div>
      <div>
        <button onClick={fetchMyAPI}>manual fetch</button>
      </div>
    </div>
  )
}

For React version >=18

Starting with React 18 you can also use Suspense, but it's not yet recommended if you are not using frameworks that correctly implement it:

In React 18, you can start using Suspense for data fetching in opinionated frameworks like Relay, Next.js, Hydrogen, or Remix. Ad hoc data fetching with Suspense is technically possible, but still not recommended as a general strategy.

If not part of the framework, you can try some libs that implement it like swr.


Oversimplified example of how suspense works. You need to throw a promise for Suspense to catch it, show fallback component first and render Main component when promise it's resolved.

let fullfilled = false;
let promise;

const fetchData = () => {
  if (!fullfilled) {
    if (!promise) {
      promise = new Promise(async (resolve) => {
        const res = await fetch('api/data')
        const data = await res.json()

        fullfilled = true
        resolve(data)
      });
    }

    throw promise
  }
};

const Main = () => {
  fetchData();
  return <div>Loaded</div>;
};

const App = () => (
  <Suspense fallback={"Loading..."}>
    <Main />
  </Suspense>
);
Antionetteantioxidant answered 1/12, 2018 at 16:2 Comment(6)
You could solve the race condition issues by checking if the component is unmounted like so: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])Ichabod
You can also use a package named use-async-effect. This package enables you to use the async await syntax.Lithomarge
Using a self invoking function not let async leak to the useEffect function definition or a custom implementation of a function that triggers the async call as a wrapper around the useEffect are the best bet for now. While you can include a new package like the suggested use-async-effect I think this is a simple problem to solve.Boudreau
hey that is fine and what I do most of the times. but eslint asks me to make fetchMyAPI() as dependency of useEffectJojo
Hello , what can i do if i get like data from localStorage using getContext , or localStorage. for eg const {authContext} = useContext(AuthContext) const data = JSON.parse(authContext).post I created async await fetch function and run inside useEffect, but that warning still comes. i've tried other methods but that warning never going :(Tieshatieup
You may change the state of an unmounted component by thatKaif
F
140

When you use an async function like

async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}

it returns a promise and useEffect doesn't expect the callback function to return Promise, rather it expects that nothing is returned or a function is returned.

As a workaround for the warning you can use a self invoking async function.

useEffect(() => {
    (async function() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    })();
}, []);

or to make it more cleaner you could define a function and then call it

useEffect(() => {
    async function fetchData() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    };
    fetchData();
}, []);

the second solution will make it easier to read and will help you write code to cancel previous requests if a new one is fired or save the latest request response in state

Working codesandbox

Fluidextract answered 16/11, 2018 at 6:11 Comment(7)
A package to make this easier has been made. You can find it here.Lithomarge
but eslint won't tolerate with thatJacquard
there is no way to execute cleanup/didmount callbackArratoon
@DavidRearte, I didn't get what you are trying to sayFluidextract
@ShubhamKhatri when you use useEffect you could return a function to do the clean up like unsubscribe to events. When you use async function you can't return anything because useEffectwill not wait the resultArratoon
@DavidRearte yes useEffect will not wait for the result before cleanup and hence in cleanup you must cancel the async requestFluidextract
are you saying i can put a clean up function in a async one? i tried but my clean up function is just never called. Can you make a little example?Arratoon
O
55

Until React provides a better way, you can create a helper, useEffectAsync.js:

import { useEffect } from 'react';


export default function useEffectAsync(effect, inputs) {
    useEffect(() => {
        effect();
    }, inputs);
}

Now you can pass an async function:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);

Update

If you choose this approach, note that it's bad form. I resort to this when I know it's safe, but it's always bad form and haphazard.

Suspense for Data Fetching, which is still experimental, will solve some of the cases.

In other cases, you can model the async results as events so that you can add or remove a listener based on the component life cycle.

Or you can model the async results as an Observable so that you can subscribe and unsubscribe based on the component life cycle.

Owings answered 11/2, 2019 at 19:27 Comment(1)
The reason React doesn’t automatically allow async functions in useEffect is that in a huge portion of cases, there is some cleanup necessary. The function useAsyncEffect as you’ve written it could easily mislead someone into thinking if they return a cleanup function from their async effect it would be run at the appropriate time. This could lead to memory leaks or worse bugs, so we opted to encourage people to refactor their code to make the “seam” of async functions interacting with React’s lifecycle more visible, and the code’s behavior as a result hopefully more deliberate and correct.Shalon
T
40

You can also use IIFE format as well to keep things short

function Example() {
    const [data, dataSet] = useState<any>(null)

    useEffect(() => {
        (async () => {
            let response = await fetch('api/data')
            response = await response.json()
            dataSet(response);
        })();
    }, [])

    return <div>{JSON.stringify(data)}</div>
}
Teel answered 20/6, 2021 at 2:22 Comment(0)
W
25

void operator could be used here.
Instead of:

React.useEffect(() => {
    async function fetchData() {
    }
    fetchData();
}, []);

or

React.useEffect(() => {
    (async function fetchData() {
    })()
}, []);

you could write:

React.useEffect(() => {
    void async function fetchData() {
    }();
}, []);

It is a little bit cleaner and prettier.


Async effects could cause memory leaks so it is important to perform cleanup on component unmount. In case of fetch this could look like this:

function App() {
    const [ data, setData ] = React.useState([]);

    React.useEffect(() => {
        const abortController = new AbortController();
        void async function fetchData() {
            try {
                const url = 'https://jsonplaceholder.typicode.com/todos/1';
                const response = await fetch(url, { signal: abortController.signal });
                setData(await response.json());
            } catch (error) {
                console.log('error', error);
            }
        }();
        return () => {
            abortController.abort(); // cancel pending fetch request on component unmount
        };
    }, []);

    return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
Whaling answered 24/5, 2020 at 0:16 Comment(3)
You know your JS, sir. AbortController is the new fangled thing that is available after cancellable promises proposal failedGodthaab
BTW there is this "use-abortable-fetch" package out there, but I'm not sure I like how it was written. It would be nice to get a simple version of this code you have here as a custom hook. Also, "await-here" is a pretty nice package that can alleviate the need for a try/catch block.Godthaab
I prefer the even shorter React.useEffect(() => { (async () => () {... })();}, []);Boutis
L
18

I read through this question, and feel the best way to implement useEffect is not mentioned in the answers. Let's say you have a network call, and would like to do something once you have the response. For the sake of simplicity, let's store the network response in a state variable. One might want to use action/reducer to update the store with the network response.

const [data, setData] = useState(null);

/* This would be called on initial page load */
useEffect(()=>{
    fetch(`https://www.reddit.com/r/${subreddit}.json`)
    .then(data => {
        setData(data);
    })
    .catch(err => {
        /* perform error handling if desired */
    });
}, [])

/* This would be called when store/state data is updated */
useEffect(()=>{
    if (data) {
        setPosts(data.children.map(it => {
            /* do what you want */
        }));
    }
}, [data]);

Reference => https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Leesen answered 26/8, 2019 at 3:53 Comment(2)
isn't it true you also need the line then(res => res.json()) ?Allness
Yes, it is, but I thin he omitted that for simplicitySanskrit
S
12

For other readers, the error can come from the fact that there is no brackets wrapping the async function:

Considering the async function initData

  async function initData() {
  }

This code will lead to your error:

  useEffect(() => initData(), []);

But this one, won't:

  useEffect(() => { initData(); }, []);

(Notice the brackets around initData()

Stalder answered 30/1, 2020 at 15:39 Comment(2)
Brilliant, man! I'm using saga, and that error appeared when I was calling an action creator which returned the only object. It looks like the useEffect the callback function doesn't lick this behavior. I appreciate your answer.Occurrence
Just in case people are wondering why this is true... Without curly braces, the return value of initData() is implicitly returned by the arrow function. With the curly braces, nothing is returned implicitly and thus the error won't happen.Benisch
J
12

For fetching from an external API using React Hooks, you should call a function that fetches from the API inside of the useEffect hook.

Like this:

async function fetchData() {
    const res = await fetch("https://swapi.co/api/planets/4/");
    res
      .json()
      .then(res => setPosts(res))
      .catch(err => setErrors(err));
  }

useEffect(() => {
    fetchData();
}, []);

I strongly recommend that you do not define your query inside the useEffect Hook, because it will be re-render infinite times. And since you cannot make the useEffect async, you can make the function inside of it to be async.

In the example shown above, the API call is in another separated async function so it makes sure that the call is async and that it only happens once. Also, the useEffect's dependency array (the []) is empty, which means that it will behave just like the componentDidMount from React Class Components, it will only be executed once when the component is mounted.

For the loading text, you can use React's conditional rendering to validate if your posts are null, if they are, render a loading text, else, show the posts. The else will be true when you finish fetching data from the API and the posts are not null.

{posts === null ? <p> Loading... </p> 
: posts.map((post) => (
    <Link key={post._id} to={`/blog/${post.slug.current}`}>
      <img src={post.mainImage.asset.url} alt={post.mainImage.alt} />
      <h2>{post.title}</h2>
   </Link>
))}

I see you already are using conditional rendering so I recommend you dive more into it, especially for validating if an object is null or not!

I recommend you read the following articles in case you need more information about consuming an API using Hooks.

https://betterprogramming.pub/how-to-fetch-data-from-an-api-with-react-hooks-9e7202b8afcd

https://reactjs.org/docs/conditional-rendering.html

Johathan answered 9/3, 2021 at 0:40 Comment(3)
Why are you using .then with await in the same block in the function definition? I thought the whole point of await was to replace .then.Brottman
I think an important thing to note about async effects is that you should handle situations where the component unmounts after the effect runs but before the callback is executed. Assuming the above fetch takes 500ms, and the component unmounts after 250ms, the callback will attempt to update state on an unmounted component, throwing an error.Archiepiscopal
no matter if the function is async or not still the warning comes upNorvell
H
5

Other answers have been given by many examples and are clearly explained, so I will explain them from the point of view of TypeScript type definition.

The useEffect hook TypeScript signature:

function useEffect(effect: EffectCallback, deps?: DependencyList): void;

The type of effect:

// NOTE: callbacks are _only_ allowed to return either void, or a destructor.
type EffectCallback = () => (void | Destructor);

// Destructors are only allowed to return void.
type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never };

Now we should know why effect can't be an async function.

useEffect(async () => {
  //...
}, [])

The async function will return a JS promise with an implicit undefined value. This is not the expectation of useEffect.

Hobie answered 10/12, 2021 at 10:37 Comment(0)
F
5

You can use this custom hook:

import { useEffect, useRef } from "react";
import lodash from "lodash";

export const useEffectAsync = (
  func: () => Promise<any>,
  dependencies: any[]
) => {
  let tasks = useRef<{ func: typeof func }[]>([]);
  const runWaitingTasks = () => {
    if (tasks.current.length) {
      tasks.current[0].func().then(() => {
        let tasksCopy = lodash.cloneDeep(tasks.current);
        tasksCopy.splice(0, 1);
        tasks.current = tasksCopy;
        runWaitingTasks();
      });
    }
  };
  useEffect(() => {
    tasks.current.push({ func });
    if (tasks.current.length === 1) {
      runWaitingTasks();
    }
  }, dependencies);
};

This hook is just created by combining the basic useEffect with a queue to manage dependencies changes asynchronously.


Simple example:

import { useState } from "react";
import { useEffectAsync ,anApiCallAsync} from "./Utils";

function App() {
  useEffectAsync(async () => {
    let response = await anApiCallAsync();
    console.log(response)
  }, []);
  return (
    <div>
      <h1>useEffectAsync!</h1>
    </div>
  );
}

export default App;

Description:

Consider this example:

import {  useState } from "react";
import { useEffectAsync } from "./Utils";

function App() {
  const [counter, setCounter] = useState(0);
  const sleep = (sleep: number) =>
    new Promise<void>((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, sleep);
    });

  useEffectAsync(async () => {
    await sleep(1500);
    console.log("useEffectAsync task with delay, counter: " + counter);
  }, [counter]);

  return (
    <div>
      <button onClick={() => setCounter(counter + 1)}>click to increase</button>
      <div>{counter}</div>
    </div>
  );
}

export default App;

enter image description here In the above example when the counter is updated, the useEffectAsync hook ensures that the previous task finishes before executing the inner function with the new counter value. Actually, if a task is currently running and the dependencies change at the same time, this custom hook effectively creates a queue for incoming tasks and executes sequentially, one after the other, with new dependency values.

This can be particularly useful for scenarios where you have time-consuming tasks and you need to ensure they complete one after the other with updated dependencies.

If you run the above example via useEffect and use the async function inside of it, every time dependencies change, the useEffect runs the inner function instantly without waiting for previous tasks to finish, potentially causing concurrency issues.

Below is the sample code with useEffect:

import { useEffect, useState } from "react";

function App() {
  const [counter, setCounter] = useState(0);
  const sleep = (sleep: number) =>
    new Promise<void>((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, sleep);
    });

  useEffect(() => {
    (async () => {
      await sleep(1500);
      console.log("useEffect task with delay, counter: " + counter);
    })();
  }, [counter]);
  return (
    <div>
      <button onClick={() => setCounter(counter + 1)}>click to increase</button>
      <div>{counter}</div>
    </div>
  );
}

export default App;

enter image description here I hope this custom hook is helpful for managing async tasks in your React components.

Floria answered 28/9, 2023 at 13:38 Comment(0)
B
4

try

const MyFunctionnalComponent: React.FC = props => {
  useEffect(() => {
    // Using an IIFE
    (async function anyNameFunction() {
      await loadContent();
    })();
  }, []);
  return <div></div>;
};
Blaisdell answered 4/12, 2019 at 8:34 Comment(0)
A
3

To do it properly and avoid errors: "Warning: Can't perform a React state update on an unmounted..."


 useEffect(() => {
    let mounted = true;
    (async () => {
      try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        const newPosts = json.data.children.map(it => it.data);
        if (mounted) {
          setPosts(newPosts);
        }
      } catch (e) {
        console.error(e);
      }
    })();
    return () => {
      mounted = false;
    };
  }, []);

OR External functions and using an object


useEffect(() => {
  let status = { mounted: true };
  query(status);
  return () => {
    status.mounted = false;
  };
}, []);

const query = async (status: { mounted: boolean }) => {
  try {
    const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
    const json = await response.json();
    const newPosts = json.data.children.map(it => it.data);
    if (status.mounted) {
      setPosts(newPosts);
    }
  } catch (e) {
    console.error(e);
  }
};

OR AbortController


 useEffect(() => {
    const abortController = new AbortController();
    (async () => {
      try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`, { signal: abortController.signal });
        const json = await response.json();
        const newPosts = json.data.children.map(it => it.data);
        setPosts(newPosts);
      } catch (e) {
        if(!abortController.signal.aborted){
           console.error(e);
        }
      }
    })();
    return () => {
      abortController.abort();
    };
  }, []);




Adelbert answered 29/6, 2022 at 9:43 Comment(0)
B
1

Please try this

 useEffect(() => {
        (async () => {
          const products = await api.index()
          setFilteredProducts(products)
          setProducts(products)
        })()
      }, [])
Bendigo answered 31/1, 2021 at 10:5 Comment(1)
this is the simplest working answer with react 18 in Oct 2022Charissa
V
1

With useAsyncEffect hook provided by a custom library, safely execution of async code and making requests inside effects become trivially since it makes your code auto-cancellable (this is just one thing from the feature list). Check out the Live Demo with JSON fetching

import React from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpFetch from "cp-fetch";

/*
 Notice: the related network request will also be aborted
 Checkout your network console
 */

function TestComponent(props) {
  const [cancel, done, result, err] = useAsyncEffect(
    function* () {
      const response = yield cpFetch(props.url).timeout(props.timeout);
      return yield response.json();
    },
    { states: true, deps: [props.url] }
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>
        {done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
      </div>
      <button className="btn btn-warning" onClick={cancel} disabled={done}>
        Cancel async effect
      </button>
    </div>
  );
}

export default TestComponent;

The same demo using axios

Vincentia answered 8/5, 2021 at 16:58 Comment(0)
L
1

I know it is late but just I had the same problem and I wanted to share that I solved it with a function like this!

useEffect(() => {
(async () => { 
  try {
   const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
   const json = await response.json();
    setPosts(json.data.children.map(it => it.data));
  } catch (e) {
console.error(e);
}
}) ()   
}, [])    
Legitimist answered 27/1, 2022 at 12:42 Comment(0)
M
0

Just create a separate async function

async function fetchdata() {
  const response = await 
  fetch(`{api_endpoint}`, {
    "method": "GET",
    "headers": { 
      "Content-Type" : "application/json",
      "Authorization": `Token ${token}`,
    },
  });
}

React does not allow useEffect to be async function. So call it from the use effect as below

useEffect(() => {
    fetchdata()
},[]);

Besides, you can look into the console log. React warning and messages includes react best practices.

Maybellmaybelle answered 16/4 at 1:48 Comment(0)
K
-1

Just a note about HOW AWESOME the purescript language handles this problem of stale effects with Aff monad

WITHOUT PURESCRIPT

you have to use AbortController

function App() {
    const [ data, setData ] = React.useState([]);

    React.useEffect(() => {
        const abortController = new AbortController();
        void async function fetchData() {
            try {
                const url = 'https://jsonplaceholder.typicode.com/todos/1';
                const response = await fetch(url, { signal: abortController.signal });
                setData(await response.json());
            } catch (error) {
                console.log('error', error);
            }
        }();
        return () => {
            abortController.abort(); // cancel pending fetch request on component unmount
        };
    }, []);

    return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

or stale (from NoahZinsmeister/web3-react example)

function Balance() {
  const { account, library, chainId } = useWeb3React()

  const [balance, setBalance] = React.useState()
  React.useEffect((): any => {
    if (!!account && !!library) {      
      let stale = false
      
      library 
        .getBalance(account)
        .then((balance: any) => {
          if (!stale) {
            setBalance(balance)
          }
        })
        .catch(() => {
          if (!stale) {
            setBalance(null)
          }
        })

      return () => { // NOTE: will be called every time deps changes
        stale = true
        setBalance(undefined)
      }
    }
  }, [account, library, chainId]) // ensures refresh if referential identity of library doesn't change across chainIds

  ...

WITH PURESCRIPT

check how useAff kills it's Aff in the cleanup function

the Aff is implemented as a state machine (without promises)

but what is relevant to us here is that:

Kenlee answered 18/9, 2021 at 13:30 Comment(0)
D
-1

Ignore the warning, and use the useEffect hook with an async function like this:

import { useEffect, useState } from "react";

function MyComponent({ objId }) {
  const [data, setData] = useState();

  useEffect(() => {
    if (objId === null || objId === undefined) {
      return;
    }

    async function retrieveObjectData() {
      const response = await fetch(`path/to/api/objects/${objId}/`);
      const jsonData = response.json();
      setData(jsonData);
    }
    retrieveObjectData();

  }, [objId]);

  if (objId === null || objId === undefined) {
    return (<span>Object ID needs to be set</span>);
  }

  if (data) {
    return (<span>Object ID is {objId}, data is {data}</span>);
  }

  return (<span>Loading...</span>);
}
Dement answered 18/11, 2021 at 5:34 Comment(0)
O
-1

The most easy way is to use useAsyncEffect from 'use-async-effect' You can find it on NPM.

const ProtectedRoute = ({ children }) => {

    const [isAuth, setIsAuth] = useState(false);

    useAsyncEffect(async () => {
        try {
            const data = await axios("auth");
            console.log(data);
            setIsAuth(true);
        } catch (error) {
            console.log(error);
        }
    }, []);



    if (!isAuth)
        return <Navigate to="/signin" />

    return children;

}
Openminded answered 2/8, 2022 at 7:24 Comment(0)
S
-1

Alternative solution with promise chaining (no async await).

useEffect(() => {
   fetch(`https://www.reddit.com/r/${subreddit}.json`)
       .then((response) => response.json())
       .then((json) => setPosts(json.data.children.map(it => it.data)))
       .catch((e) => console.error(e));
}, []);
Shemeka answered 12/2 at 11:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.