0

I am using Create-React-App and the (excellent) use-http for a custom useFetch hook. The goal is to make several API calls upon login to an account area:

const [user, setUser] = useState(null)
const [profile, setProfile] = useState(null)
const [posts, setPosts] = useState(null)

const request = useFetch('/')

const initializeAccount = async () => {
  try {
    const user = await request.get('api/user/')
    const profile = await request.get('api/profile/')
    const posts = await request.get('api/posts/')
    if (user) {
      setUser(user.data)
    }
    if (profile) {
      setProfile(profile.data)
    }
    if (posts) {
      setPosts(posts.data)
    }
  } catch (e) {
    console.log('could not initialize account')
  }
}

useEffect(() => {
  initializeAccount()
  return () => console.log('unmount')
})

I have tried using [] as the dependency array, but I get a linting error saying to move initializeAccount to the dependency array. If I add it, the function runs endlessly.

What is the correct way to setup the dependency array so that this function is called one time? Also, what would be the correct way to handle abort of each of the API calls in this scenario?

1
  • Not sure if useFetch will return same reference every time but you could try the following: const initializeAccount = useCallback(async () => {,,,},[request]); And pass initializeAccount to the useEffect. Commented Sep 23, 2019 at 18:44

2 Answers 2

2

My man, in order to run useEffect once for api calls, you have to do it like this:

useEffect(() => {
   initializeAccount()
   return () => console.log('unmount')
},[])

Hope it helps.

Sign up to request clarification or add additional context in comments.

6 Comments

If I do this I get a linting error saying to add the dependency to the array - does that mean the linting error is incorrect? Also, what would I return in useEffect to abort the fetch calls?
@Toby, that is in the documentation request.abort()
@HMR I had read that, but I wasn't sure exactly how to us it - I'll give this a try, thanks.
@Toby that lint rule is still fairly new, and very blunt, just add a lint override for the line. I also recommend leaving a comment above that as for the reason for the override, only firing hook on mount is a pretty legit reason. I believe the authors of the react-hooks eslinter are looking into more fine-grained control/rules, may just need to wait a while.
@DrewReese I wondered if that was the case, since the empty dependency array does work - but I didn't want to ignore and cause an issue down the line. Thanks!
|
0

The lint error is there for a reason and ignoring it might cause bugs that are very hard to find.

No dependency array

If you exclude the dependency array completely (useEffect(() => { /* ... */ });) the effect will run on every render, causing an infinite loop.

Empty dependency array

If you add an empty dependency array (useEffect(() => { /* ... */ }, [])) the effect will only run on first render. That might work here but it will cause issues in other scenarios, or if something change in the future. E.g. say that request takes some time to initialize (so it's first undefined, to later change to an object) and that initializeAccount looks like below. Now initalizeAccount would run only on first render with request === undefined, when request is ready in a later stage and the component is re-rendered, initalizeAccount won't run again and the functionality will silently fail.

const initializeAccount = async () => {
  if (request) {
    const user = await request.get('api/user/')
    if (user) {
      setUser(user.data)
    }
  }
}

Populated dependency array (the right way)

If you add initializeAccount to the dependency array (useEffect(() => { /*...*/ }, [initalizeAccount])) the effect will run every time initializeAccount is changed. For objects (and functions) React compares the reference from the last render with the reference from the current render to determine if the object has changed. The way you define initializeAccount it will be redefined on every render and hence the reference will be new and the effect will run on every render, just as you experience.

What you need to do is to obtain a stable reference to initalizeAccount. For this React provides useCallback, it is similar to useEffect in that it has a dependency array. If we define initializeAccount with useCallback, React will make sure that the reference is the same between renders. It is only when one of the dependencies in the dependency array of useCallback change that the function is redefined which in turn triggers the useEffect to run again. This way you are adhering to the lint rule while at the same time the effect is only run once.

const initializeAccount = useCallback(async () => {
  if (request) {
    const user = await request.get('api/user/')
    if (user) {
      setUser(user.data)
    }
  }
}, [request])

Note that if request, in turn, doesn't have a stable reference, useCallback will be run on each render (as described above) and then so will useEffect. If useFetch is written by you, make sure it returns an object with a stable reference. If useFetch is provided from a library, that is most likely the case already.

TL;DR

Wrap initializeAccount in useCallback and add it to the dependency array of useEffect.

const initializeAccount = useCallback(async () => {
  try {
    const user = await request.get('api/user/')
    const profile = await request.get('api/profile/')
    const posts = await request.get('api/posts/')
    if (user) {
      setUser(user.data)
    }
    if (profile) {
      setProfile(profile.data)
    }
    if (posts) {
      setPosts(posts.data)
    }
  } catch (e) {
    console.log('could not initialize account')
  }
}, [request])

useEffect(() => {
  initializeAccount()
  return () => console.log('unmount')
}, [initializeAccount])

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.