Reputation: 1241
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?
Upvotes: 113
Views: 271116
Reputation: 4040
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.
Upvotes: 7
Reputation: 342
I had a similar problem with the same error message, where the order of variable declarations was the source of the error:
if (loading) return <>loading...</>;
if (error) return <>Error! {error.message}</>;
const [reload, setReload] = useState(false);
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
Upvotes: 15
Reputation: 3868
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);
Upvotes: 1
Reputation: 22304
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
}
Upvotes: 116
Reputation: 703
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.
Upvotes: 2
Reputation: 1701
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()]);
Upvotes: 24