2

I'm a bit baffled here. According to this SO question and also this question, I should be able to call the useEffect hook after calling the useQuery hook to populate state, which I am doing via useReducer. E.g., something like this (assume that foo, bar, and baz are named queries within a single request to the GraphQL server):

const MyComponent = ( props ) => {

  const [ state, dispatch ] = useReducer( reducer, initialState )

  const query = someCondition ? query1 : query2
  const variables = { variables: someCondition ? query1Vars : query2Vars }

  const { loading, error, data } = useQuery( query, variables )

  useEffect(function() {
    dispatch({
      type: types.INIT,
      payload: {
        foo: data.foo,
        bar: data.bar,
        baz: data.baz
      }
    })
  }, [data])

  return (
    <div>
    {
      (() => {
         if ( loading ) return <Loading />
         if ( error ) return <Error />
         return (
           // components that depend on data
         )
      })()
    }
    </div>
  )
}

For reasons I can't determine, data is undefined when I reference it inside useEffect. I have checked my network responses and the call to my GraphQL endpoint is indeed returning data. (Essentially, what's described in this other SO question about useQuery.)

Except for the conditional query/variable assignment, this code is in use for some of our other components and it works fine, so I have no idea why it's not working in my case.

I'm not sure what's going on here, as I've made sure that no hooks are being called conditionally, in violation of the rules of hooks; all I'm doing is conditionally assigning variable values before sending the query via Apollo client. (I also tried the onCompleted property of the useQuery options, without the useEffect hook, but to no avail; data is still undefined.)

diekunstderfuge
  • 521
  • 1
  • 7
  • 15
  • This looks like expected behaviour to me? `useEffect` will always fire before `useQuery` has completed, what I would expect is that `useEffect` would trigger twice, once when the component mounts and then secondly when `useQuery` returns a result. Seems to me that you aren't accounting for the initial `useEffect` call when `data` has yet to be set. – James Jan 31 '20 at 17:28
  • 1
    Hmm, adding a check for `if ( data ) { ... }` doesn't seem to account for the initial firing of the `useEffect` hook. https://github.com/trojanowski/react-apollo-hooks/issues/158 seems to suggest you don't need to use `useEffect` at all, but I've seen that code working previously. How should the initial render be handled? – diekunstderfuge Jan 31 '20 at 17:43
  • 1
    correct, you don't because `useQuery` already stores the state for you - however I've no idea of your use case, only going off the code you provided where you want to get it into some form of reducer. – James Jan 31 '20 at 17:49
  • `data` will be initially undefined as @James pointed out. It can also be undefined indefinitely if network errors are encountered. That said, there's little reason to ever do this -- `useQuery` already manages state for you. If you need to further transform this state, then you just do so as regular variables inside your component. – Daniel Rearden Jan 31 '20 at 19:06
  • Thanks @DanielRearden, as I mention in my comment below, I changed some rendering with my child components, which were apparently rendering even when the state wasn't there to support them. I did use the `onCompleted` callback for `useQuery`, but I see so many examples of `useQuery` + `useEffect` that I was certain it should work as described, or least not be actively problematic. :) – diekunstderfuge Jan 31 '20 at 20:18
  • First check if `loading` then data won't be defined. then, even if the query did load, it could return null data, so you have to check that condition too. Yes, the hooks unconditional calling rule forces you to check data everywhere you use it, at least up to the last hook you call. You can't assume data to be defined. – Petruza Aug 12 '21 at 16:07

1 Answers1

3

Unless I'm not fully understanding your issue, this looks like expected behaviour.

useEffect will fire on mount and then anytime data changes, data will only change when the API call has completed. The initial useEffect call is going to happen before useQuery has gotten a response from the server therefore you can't assume inside useEffect that data will be set.

If you only want to dispatch when data is there then you should protect the call e.g.

useEffect(() => {
  if (data) {
    dispatch({
      type: types.INIT,
      payload: {
        foo: data.foo,
        bar: data.bar,
        baz: data.baz
      }
    });
  }
}, [data]);
James
  • 80,725
  • 18
  • 167
  • 237
  • 1
    Yes, I'd tried that check but `data` still seems to be undefined (the actual error just shows up a little further downstream). – diekunstderfuge Jan 31 '20 at 17:46
  • @diekunstderfuge how many times does `useEffect` fire? Is it twice as expected? It would suggest the issue lies in `useQuery` then and not `useEffect` tbh – James Jan 31 '20 at 17:47
  • It looks as if `useEffect` is only firing once, which according to the docs should only happen if an empty array is passed as the second parameter. – diekunstderfuge Jan 31 '20 at 18:40
  • 1
    I went back to the `onCompleted` option of `useQuery` and did my `dispatch` calls there, being sure to check for the piece of data that I needed. According to Apollo, the `onCompleted` callback should only execute on *successful* completion of the query (I defined an `onError` callback just in case). It works, now that I've changed some criteria for rendering child components. I'll mark this answer as accepted and, if I have time, see if it works with the modifications I made to my rendering. Thanks! – diekunstderfuge Jan 31 '20 at 20:10
  • 2
    @diekunstderfuge *"according to the docs should only happen if an empty array is passed"* - this isn't the *only* scenario where this would happen, the second parameter to `useEffect` is a list of trigger dependencies, in other words, if any items in the array change at any point then `useEffect` will re-trigger - an empty array is just an easy way of making sure `useEffect` only ever fires once (an empty array will never change). In this example, we are seeing that `useQuery` doesn't appear to force a state change therefore as far as `useEffect` is concerned, `data` hasn't changed. – James Jan 31 '20 at 22:26