I was converting react class components to functional components in one of my projects.
This is the class component
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null,
}
}
componentDidMount() {
this.func1().then((res) => {
this.func2();
})
}
async func1() {
this.setState({
user: "ishan"
})
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const data = await res.json();
return data;
}
func2() {
console.log("user state: ",this.state.user);
}
render() {
return (
<div className='App'>
<h1>Hello World</h1>
</div>
);
}
}
This gives the following output in the console
user state: ishan
This is the corresponding functional component
export const App = () => {
const [user, setUser] = useState(null);
useEffect(() => {
func1().then((res) => {
func2();
});
}, []);
const func1 = async () => {
setUser("ishan");
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const data = await res.json();
return data;
};
const func2 = () => {
console.log("user state: ",user);
};
return (
<div className="App">
<h1>Hello World</h1>
</div>
);
};
This gives the following output in the console:
user state: null
I'm not sure why the user state is null in case of functional component, the flow of logic is similar in both the cases. Please explain the difference in the output.
CodePudding user response:
The problem seems to be in closures.
Everything inside the useEffect
s callback will only ever run on mount, hence, user
will always be null
there because closures "remembered" only its initial state.
One way to workaround this issue would be to split your useEffect
into 2:
useEffect(() => {
func1();
}, []);
useEffect(() => {
func2();
}, [user]);
In this case, func2
will run each time user
changes.
CodePudding user response:
Each render has it's own state, effect, props, and everything else
- according to this article from Dan Abramov's blog
In order to better understand this, we can put a log statement outside func2
like this:
export const App = () => {
const [user, setUser] = useState(null);
useEffect(() => {
func1().then((res) => {
func2();
});
}, []);
const func1 = async () => {
setUser("ishan");
};
const func2 = () => {
console.log("user state: ",user);
};
console.log("user state from outside: ",user); // <- put a log statement outside
return (
<div className="App">
<h1>Hello World</h1>
</div>
);
};
And reload the app, you will get this output:
user state from outside: null
user state from outside: null
user state: null
user state: null
user state from outside: ishan
user state from outside: ishan
Both user state: null
and user state from outside: null
are expected to have a null value, as the first render has a user state as null, and both log statement are pointing the state at that particular render, but the outside log statement
has a updated state logged out.
That's because the state gets updated by func1
, causing component to rerender and thus outside log gets executed, and as opposed to initial render's null value, this render(completely different from the initial render) has an updated value. As useEffect has an empty dependency, it only runs once after initial render, and inside it func2
won't run this time.