How do you handle useQuery with useEffect?

Hi all,

I feel I’m having trouble with a very basic case, but I can’t find any elegant solution.
I need to edit a form with data coming from useQuery.
My issue is that react imposes an order to execute hooks, and useEffect must be before useQuery, which makes it impossible for me to avoid infinite loop when setting my state directly in the component function. Here’s the code:

I could use the onCompleted() callaback but I’m pretty sure the hook was designed to work without a hack like this right?

export default function MyForm( props ) {
    const [title, setTitle] = useState()
    const { loading, data, error } = useQuery(COMPLAINT, {variables: {issueId: props.issueId}})
    const classes = useStyles()
    const {t} = useTranslation('common')

    if (loading) {
        logger.debug("loading ", props.issueId)
        return "loading..."
    }
    if (error) {
        logger.debug("error ", props.issueId, error)
        // setError(isError)
        return <div>{"error " + error}</div>;
    }
// when put here, it throws the error pasted below, if put before useQuery, it executes before data is loaded. If the code is placed directly in the function it creates an infinite loop
    useEffect(() => {
        logger.debug("checking id data is null ", data)
        if(data) {
            logger.debug("data loaded : ", data)
            issue = data.complaintById
            setTitle(issue.title)
        }
    }, [title])

    return renderEditor(state)
}

The error when the useEffect function is put after useQuery

Warning: React has detected a change in the order of Hooks called by EditIssue. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks

   Previous render            Next render
   ------------------------------------------------------
1. useState                   useState
2. useState                   useState
3. useContext                 useContext
4. useReducer                 useReducer
5. useRef                     useRef
6. useRef                     useRef
7. useEffect                  useEffect
8. useEffect                  useEffect
9. useContext                 useContext
10. useDebugValue             useDebugValue
11. useContext                useContext
12. useRef                    useRef
13. useRef                    useRef
14. useRef                    useRef
15. useMemo                   useMemo
16. useEffect                 useEffect
17. useEffect                 useEffect
18. useDebugValue             useDebugValue
19. useContext                useContext
20. undefined                 useEffect
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    at EditIssue (http://localhost:3000/_next/static/chunks/pages/request/%5BissueId%5D/edit.js?ts=1624015172315:827:74)
    at EditRequest (http://localhost:3000/_next/static/chunks/pages/request/%5BissueId%5D/edit.js?ts=1624015172315:155935:77)
    at ThemeProvider (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624015172315:12966:24)
    at Provider (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624015172315:42887:24)
    at ApolloProvider (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624015172315:6003:21)
    at App (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624015172315:56784:24)
    at AppWithTranslation
    at ErrorBoundary (http://localhost:3000/_next/static/chunks/main.js?ts=1624015172315:677:47)
    at ReactDevOverlay (http://localhost:3000/_next/static/chunks/main.js?ts=1624015172315:781:23)
    at Container (http://localhost:3000/_next/static/chunks/main.js?ts=1624015172315:9119:5)
    at AppContainer (http://localhost:3000/_next/static/chunks/main.js?ts=1624015172315:9608:24)
    at Root (http://localhost:3000/_next/static/chunks/main.js?ts=1624015172315:9741:24)

Hi there! The error message is a bit ambiguous - here it’s not really about the order of hooks but the fact that your useEffect hook is conditional. Every render of your component needs to have the same number of hooks (and have them be in the same order). Your early returns in your loading and error checks cause your useEffect hook to be available in some renders but not in others, which conflicts with how hooks are supposed to be used. You will have to move your hook up so it’s available in every render. It doesn’t have to be before useQuery, though, just somewhere before the if statements.

This does mean that the hook may be executed before the query data is available, but that is fine - you’ll just have to handle these cases inside your useEffect hook, as you already seem to be doing, considering your data check.

3 Likes

Hello MindNektar,

Thanks for the explanations, I’m still migrating from classes to hooks, even though I knew we shouldn’t put hooks under conditions, my perception of useEffect was like it’s not really a hook, it’s just a garbage collector :slight_smile: :smiley:

So yeah, it works better for me, but I still get blank pages sometimes where loading state change seems not to trigger useEffect :

function EditIssue(props) {
    const [state, setState] = useState({
            reset: true,
            workingStatus: false,
        }
    )
    const [issue, setIssue] = useState()

    useEffect(() => {
        logger.debug("checking if data is null ", data)
        if (data) {
            logger.debug("data loaded : ", data)
            setIssue({...(data.complaintById)})
        }
    }, [loading])

    const {loading, data, error} = useQuery(COMPLAINT, {variables: {issueId: props.issueId}})
    const classes = useStyles()
    const {t} = useTranslation('common')
    const router = props.router

    if (loading) {
        logger.debug("loading ", props.issueId)
        return "loading..."
    }
    if (error) {
        logger.debug("error ", props.issueId, error)
        // setError(isError)
        return <div>{"error " + error}</div>;
    }
    if (!issue)
        return "still loading"

    const cacheData = null
    let playerRef

    if (issue)
        return renderEditor(state)
}

I can’t figure when it works and when it hangs unnotified, one thing is kind of systematic: when I load the page from server (ssr with next) it remains blank with the message: “still loading” (and "checking if data is null " as the last log message. (I suspect it to be a timing ‘call order’ issue)

Please note that this code always works, I’m just trying to do things right since I’m migrating my old code to introduce hooks. So I’d like to use the right pattern

    const { loading, data, error } = useQuery(COMPLAINT, {variables: {issueId: props.issueId}, onCompleted:(data)=>{
            logger.debug("checking id data is null ", data)
            if(data) {
                logger.debug("data loaded : ", data)
                setIssue( {...(data.complaintById)} )
            }
        }})

Thanks for your answer, I just discovered the answer button. Now that I read the error message again, I understand it better thanks to your explanations :slight_smile: