72

React is complaining about code below, saying it useEffect is being called conditionally:

import React, { useEffect, useState } from 'react'
import VerifiedUserOutlined from '@material-ui/icons/VerifiedUserOutlined'
import withStyles from '@material-ui/core/styles/withStyles'
import firebase from '../firebase'
import { withRouter } from 'react-router-dom'

function Dashboard(props) {
  const { classes } = props
  
  const [quote, setQuote] = useState('')

    if(!firebase.getCurrentUsername()) {
        // not logged in
        alert('Please login first')
        props.history.replace('/login')
        return null
    }

    useEffect(() => {
        firebase.getCurrentUserQuote().then(setQuote)
    })

    return (
        <main>
            // some code here
        </main>
    )

    async function logout() {
        await firebase.logout()
        props.history.push('/')
    }
}

export default withRouter(withStyles(styles)(Dashboard))

And that returns me the error:

React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render.

Does anyone happen to know what the problem here is?

Penny Liu
  • 11,885
  • 5
  • 66
  • 81
  • 2
    return null? from if condition? A component can only return valid JSX – A dev Aug 23 '19 at 06:34
  • 5
    @NatGeo `null` is a valid JSX expression https://stackoverflow.com/q/42083181/1176601 ... but the code after return is only executed when the `if` statement is false, similar to `else { ... }` - a.k.a. "conditionally" which is forbidden by rules-of-hooks – Aprillion Aug 23 '19 at 06:51

5 Answers5

81

Your code, after an if statement that contains return, is equivalent to an else branch:

if(!firebase.getCurrentUsername()) {
    ...
    return null
} else {
    useEffect(...)
    ...
}

Which means that it's executed conditionally (only when the return is NOT executed).

To fix:

useEffect(() => {
  if(firebase.getCurrentUsername()) {
    firebase.getCurrentUserQuote().then(setQuote)
  }
}, [firebase.getCurrentUsername(), firebase.getCurrentUserQuote()])

if(!firebase.getCurrentUsername()) {
  ...
  return null
}
Aprillion
  • 19,631
  • 4
  • 54
  • 87
17

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. You can follow the documentation here.

I couldn't find the use case in the above code. If you need the effect to run when the return value of firebase.getCurrentUsername() changes, you might want to use it outside the if condition like:

useEffect(() => {
    firebase.getCurrentUserQuote().then(setQuote)
}, [firebase.getCurrentUsername()]);
Vishnu
  • 1,371
  • 1
  • 11
  • 23
2

The issue here is that when we are returning null from the if block, the useEffect hook code will be unreachable, since we returned before it, and hence the error that it is being called conditionally.

You might want to define all the hooks first and then start writing the logic for rendering, be it null or empty string, or a valid JSX.

Anuradha Kumari
  • 683
  • 5
  • 9
2

I had a similar problem with the same error message, where the order of variable declarations was the source of the error:

Bad example

if (loading) return <>loading...</>;
if (error) return <>Error! {error.message}</>;

const [reload, setReload] = useState(false);

Good example

const [reload, setReload] = useState(false);

if (loading) return <>loading...</>;
if (error) return <>Error! {error.message}</>;

The hook needs to be created before potential conditional return blocks

GoWithTheFlow
  • 110
  • 2
  • 8
1

I would argue there is a way to call hooks conditionally. You just have to export some members from that hook. Copy-paste this snippet in codesandbox:

import React from "react";
import ReactDOM from "react-dom";

function useFetch() {
  return {
    todos: () =>
      fetch("https://jsonplaceholder.typicode.com/todos/1").then(response =>
        response.json()
      )
  };
}

const App = () => {
  const fetch = useFetch(); // get a reference to the hook

  if ("called conditionally") {
    fetch.todos().then(({title}) => 
      console.log("it works: ", title)); // it works:  delectus aut autem  
  }

  return null;
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Here's an example with a wrapped useEffect:

import React, { useEffect } from "react";
import ReactDOM from "react-dom";

function useWrappedEffect() {
  const [runEffect, setRunEffect] = React.useState(false);

  useEffect(() => {
    if (runEffect) {
      console.log("running");
      setRunEffect(false);
    }
  }, [runEffect]);

  return {
    run: () => {
      setRunEffect(true);
    }
  };
}

const App = () => {
  const myEffect = useWrappedEffect(); // get a reference to the hook
  const [run, setRun] = React.useState(false);

  if (run) {
    myEffect.run();
    setRun(false);
  }

  return (
    <button
      onClick={() => {
        setRun(true);
      }}
    >
      Run
    </button>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Tudor Morar
  • 3,244
  • 1
  • 25
  • 24
  • Your `useFetch` is not a hook. It does not use `useState` or `useEffect` (or any other React core hooks) – DLight Mar 16 '21 at 10:12
  • I kept the example to a minimum. Feel free to make use of useState inside if you like – Tudor Morar Mar 17 '21 at 10:17
  • The question is about `useEffect`, could you update your example to use that? Because I don't see how it would work. – DLight Mar 18 '21 at 11:55
  • Thanks! But it's misleading, you don't "have to export some members from that hook" to trigger the `useEffect` conditionally. As I understand, the key is to put an `if` inside the `useEffect`. (Also I see you used two states, we don't need both) – DLight Mar 21 '21 at 21:20
  • Yes the key is to put an `if` statement inside `useEffect`. I know my answer is not exactly the solution to the problem, but overall it's a powerfull concept since you can extract logic in a separate custom hook (not actually the native `useEffect`) and use it wherever afterwords – Tudor Morar Mar 22 '21 at 15:24