React Hook "useEffect" is called conditionally

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?

167824 次浏览

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()]);

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.

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
}

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);

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

you can not call hooks conditionally because React relies on the order in which Hooks are called

you can refer rules of hooks from react official docs https://reactjs.org/docs/hooks-rules.html#explanation

Explanation we can use multiple State or Effect Hooks in a single component:

function Form() {
// 1. Use the name state variable
const [name, setName] = useState('Mary');


// 2. Use an effect for persisting the form
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});


// 3. Use the surname state variable
const [surname, setSurname] = useState('Poppins');


// 4. Use an effect for updating the title
useEffect(function updateTitle() {
document.title = name + ' ' + surname;
});


// ...
}

So how does React know which state corresponds to which useState call? The answer is that React relies on the order in which Hooks are called. Our example works because the order of the Hook calls is the same on every render:

// ------------
// First render
// ------------
useState('Mary')           // 1. Initialize the name state variable with 'Mary'
useEffect(persistForm)     // 2. Add an effect for persisting the form
useState('Poppins')        // 3. Initialize the surname state variable with 'Poppins'
useEffect(updateTitle)     // 4. Add an effect for updating the title


// -------------
// Second render
// -------------
useState('Mary')           // 1. Read the name state variable (argument is ignored)
useEffect(persistForm)     // 2. Replace the effect for persisting the form
useState('Poppins')        // 3. Read the surname state variable (argument is ignored)
useEffect(updateTitle)     // 4. Replace the effect for updating the title


// ...

As long as the order of the Hook calls is the same between renders, React can associate some local state with each of them. But what happens if we put a Hook call (for example, the persistForm effect) inside a condition?

 // 🔴 We're breaking the first rule by using a Hook in a condition
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}

The name !== '' condition is true on the first render, so we run this Hook. However, on the next render the user might clear the form, making the condition false. Now that we skip this Hook during rendering, the order of the Hook calls becomes different:

useState('Mary')           // 1. Read the name state variable (argument is ignored)
// useEffect(persistForm)  // 🔴 This Hook was skipped!
useState('Poppins')        // 🔴 2 (but was 3). Fail to read the surname state variable
useEffect(updateTitle)     // 🔴 3 (but was 4). Fail to replace the effect

React wouldn’t know what to return for the second useState Hook call. React expected that the second Hook call in this component corresponds to the persistForm effect, just like during the previous render, but it doesn’t anymore. From that point, every next Hook call after the one we skipped would also shift by one, leading to bugs.

This is why Hooks must be called on the top level of our components. If we want to run an effect conditionally, we can put that condition inside our Hook:

  useEffect(function persistForm() {
// 👍 We're not breaking the first rule anymore
if (name !== '') {
localStorage.setItem('formData', name);
}
});

Note that you don’t need to worry about this problem if you use the provided lint rule. But now you also know why Hooks work this way, and which issues the rule is preventing.