Unmount cycle behavior in react hooks - reactjs

I have parent and child components like below:
Parent
import React, { useEffect, useState } from "react";
import Cmp1 from "./Cmp1";
const App = () => {
console.log("render App");
const [parentState, setParentState] = useState(null);
const updateParent = state => {
setParentState(state);
};
useEffect(() => {
console.log("mount App");
}, []);
useEffect(() => {
console.log("update App");
return () => {
console.log("unmount App");
};
}, [parentState]);
return (
<div>
<h1>{parentState}</h1>
<Cmp1 updateParent={updateParent} />
</div>
);
};
export default App;
Child
import React, { useEffect } from "react";
const Cmp1 = ({ updateParent }) => {
console.log('render Cmp1')
useEffect(() => {
console.log("mount Cmp1");
updateParent('From Cmp1')
}, []);
return <h1>Cmp1</h1>;
};
export default Cmp1;
When rendering these two components, I added some life cycle hooks and here is the output
render App
render Cmp1
mount Cmp1
mount App
update App
render App
render Cmp1
unmount App // <= no idea why this came out
update App
Here is the codesanbox.
I didn't unmount the component and just did a simple state update from child to parent, I was not expecting that line and I have no idea why this unmount cycle is running here.
Since I'm new to react hooks, could someone please explain this?

Your hook is dependent on parentState, so the returned function is not only running on component unmount. Its a normal clean up function that gets called every time before the hook is called again and again on unmount.
To log when the component unmounts, you need to use an effect with [] dependencies.
useEffect(() => {
return () => {
console.log('unmount');
}
}, [])
From the docs (my bold):
When exactly does React clean up an effect? React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time. We’ll discuss why this helps avoid bugs and how to opt out of this behavior in case it creates performance issues later below.

Related

Why is my boolean state value not toggling?

I know there are other articles and posts on this topic and almost all of them say to use the ! operator for a Boolean state value. I have used this method before but for the life of me I can not toggle this Boolean value.
import { useState } from 'react';
const [playerTurn, setPlayerTurn] = useState(true);
const changePlayerTurn = () => {
console.log(playerTurn); // returns true
setPlayerTurn(!playerTurn);
console.log(playerTurn); // also returns true
};
changePlayerTurn();
I have also tried setPlayerTurn(current => !current), commenting out the rest of my code to avoid interference, and restarted my computer in case that would help but I am still stuck with this issue.
Can anyone point out why this is not working?
The setPlayerTurn method queues your state change (async) so reading the state directly after will provide inconsistent results.
If you use your code correctly in a react component you will see that playerTurn has changed on the next render
You creating a async function, to solve this you can create a button in your component, which will run the function and you can use the "useEffect" hook to log every time the boolean changes... so you can see the changes taking place over time, like this:
import React, { useEffect } from "react";
import { useState } from "react";
const Player = () => {
const [playerTurn, setPlayerTurn] = useState(true);
useEffect(() => {
console.log(playerTurn);
}, [playerTurn]);
return <button onClick={() => setPlayerTurn(!playerTurn)}>change player turn</button>;
};
export default Player;
This is happening because setPlayerTurn is async function.
You can use another hook useEffect() that runs anytime some dependencies update, in this case your playerTurn state.
export default YourComponent = () => {
const [playerTurn, setPlayerTurn] = useState(true);
useEffect(() => {
console.log('playerTurn: ', playerTurn);
}, [playerTurn]);
const changePlayerTurn = () => {
setPlayerTurn(!playerTurn);
}
return (
<button onClick={changePlayerTurn}>Click to change player turn</button>
);
}
Basically whenever you use setState React keeps a record that it needs to update the state. And it will do some time in the future (usually it takes milliseconds). If you console.log() right after updating your state, your state has yet to be updated by React.
So you need to "listen" to changes on your state using useEffect().
useEffect() will run when your component is first mounted, and any time the state in the dependencies array is updated.
The value of the state only changes after the render. You can test this like:
// Get a hook function
const Example = ({title}) => {
const [playerTurn, setPlayerTurn] = React.useState(true);
React.useEffect(() => {
console.log("PlayerTurn changed to", playerTurn);
}, [playerTurn]);
console.log("Rendering...")
return (<div>
<p>Player turn: {playerTurn.toString()}</p>
<button onClick={() => setPlayerTurn(!playerTurn)}>Toggle PlayerTurn</button>
</div>);
};
// Render it
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
The callback inside the useEffect runs during the component mount and when one of the values inside the second argument, the dependecy array, changes. The depency here is playerTurn. When it changes the console will log.
As you will see, before this happens, the "Rendering..." log will appear.

Does setState function of useState hook trigger re-render of whole component or just the returned JSX part of the code?

I am a beginner in React and I am learning it from a udemy course. Initially I thought the whole code inside the functional component gets re rendered/re run after state update. But then I saw this implementation of countdown before redirect and got confused.
import React, { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
const LoadingToRedirect = () => {
const [count, setCount] = useState(5);
let history = useHistory();
useEffect(() => {
const interval = setInterval(() => {
setCount((currentCount) => --currentCount);
}, 1000);
// redirect once count is equal to 0
count === 0 && history.push("/");
// cleanup
return () => clearInterval(interval);
}, [count]);
return (
<div className="container p-5 text-center">
<p>Redirecting you in {count} seconds</p>
</div>
);
};
export default LoadingToRedirect;
So why is setInterval needed here if setCount triggers a re-render of the whole code? Can't we do the same thing with setTimeout? So I tried that and it worked. Surely there is a reason he did it that way? Am I missing something?
Of course React re-renders the whole component but it also depends on some conditions. For example if you look at your code you have passed count variable as a dependency to useEffect hook, it means if the value of count changes React will render the effect inside of the useEffect hook.
Yes, you can achieve the same using setTimeout;setInterval is
pointless because it totally depends on count variable you passed as a
dependency.
if you remove count as a dependency then you can easily see it will
not redirect you the required page.

Pass function to Context API

I'm dealing with a mix of function components and class components. Every time a click happens in the NavBar I want to trigger the function to validate the Form, it has 5 forms, so each time I'm going to have to set a new function inside the context API.
Context.js
import React, { createContext, useContext, useState } from "react";
const NavigationContext = createContext({});
const NavigationProvider = ({ children }) => {
const [valid, setValid] = useState(false);
const [checkForm, setCheckForm] = useState(null);
return (
<NavigationContext.Provider value={{ valid, setValid, checkForm, setCheckForm }}>
{children}
</NavigationContext.Provider>
);
};
const useNavigation = () => {
const context = useContext(NavigationContext);
if (!context) {
throw new Error("useNavigation must be used within a NavigationProvider");
}
return context;
};
export { NavigationProvider, useNavigation, NavigationContext};
Form.js
import React, { Component } from "react";
import { NavigationContext } from "../hooks/context";
class Something extends Component {
static contextType = NavigationContext;
onClickNext = () => {
// This is the funcion I want to set inside the Context API
if(true){
return true
}
return false;
};
render() {
const { setCheckForm } = this.context;
setCheckForm(() => () => console.log("Work FFS"));
return (
<>
<Button
onClick={this.onClickNext}
/>
</>
);
}
}
export default Something;
The problem when setting the function it throws this error:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
And setting like setCheckForm(() => console.log("Work FFS"));, it triggers when rendered.
Render method of React.Component runs whenever state changes and setCheckForm updates the state whenever that render happens. This creates an infinite loop, this is the issue you are having there.
So, this is a lifecycle effect, you have to use that function inside componentDidMount if you want to set it when the component first loads.
While this solves your problem, I wouldn't suggest doing something like this. React's mental model is top to bottom, data flows from parent to child. So, in this case, you should know which component you are rendering from the parent component, and if you know which component to render, that means you already know that function which component is going to provide to you. So, while it is possible in your way, I don't think it is a correct and Reactish way to handle it; and it is probably prone to break.

useEffect of children component called before useEffect of parent

I am trying to understand why the useEffect of a children component gets called before the Parent component useEffect.
From my understanding, useEffect shoulde be called in the order they are defined based on React's documentation:
React will apply every effect used by the component, in the order they were specified.
This would mean, a Parent's useEffect should be called before a Children's useEffect, but this is not the case.
Example:
const MainComponent = () => {
return {
<ParentComponent />
}
const ParentComponent = () => {
useEffect(() => {
console.log('parent');
}, []);
return <div>Parent <ChildrenComponent /></div>;
}
const ChildrenComponent = () => {
useEffect(() => {
console.log('children');
}, []);
return <div>Children</div>;
}
If you check the console, you should see first children and then parent
Live Code: https://codesandbox.io/s/crazy-butterfly-yn046?file=/src/App.js
My gut tells me this has to do with how react does Layout and Paint of the Parent-Children components?
This:
React will apply every effect used by the component, in the order they were specified.
Would be more precisely stated as:
React will apply every effect used by the component, in the order they were specified in that component.
For example:
const SomeComponent = () => {
useEffect(() => {
console.log('This will run first');
});
useEffect(() => {
console.log('This will run second');
});
// ...
is guaranteed to run in order.
It's not saying anything about the order that effects in different components run.

How to specify a constructor with a functional component (fat arrow syntax)?

Given this component:
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
const NewGoalInput = props => {
return (
<input type="text" onKeyUp={handleKeyUp}/>
)
}
const handleKeyUp = (e) => {
if (e.key === "Enter") {
// TODO Add goal
}
}
export default NewGoalInput
How do I add a constructor where I can define the state without using the extends React.Component syntax?
Since it's a stateless component it doesn't have the component lifecycle.
Therefor you can't specify a constructor.
You have to extend React.Component to create a stateful component which then will need a constructor and you'll be able to use the state.
Update
Since React 16.8.0 and Hooks got introduced there are more options.
Hooks are a new feature proposal that lets you use state and other React > features without writing a class. They are released in React as a part of > v16.8.0
Stateless:
import React from "react"
const Stateless = ({name}) => (
<div>{`Hi ${name}`}</div>
);
Stateful:
Has access to component lifecycle methods and local state.
class Stateful extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
const { count } = this.state;
document.title = `You've clicked ${count} times.`;
}
componentDidUpdate() {
const { count } = this.state;
document.title = `You've clicked ${count} times.`;
}
render() {
const { count } = this.state;
return (
<div>
<p>You've clicked {count} times.</p>
<button onClick={() => this.setState({ count: count + 1 })}>
Click me
</button>
</div>
);
}
}
Using Hooks:
Able to use State Hook and Effect Hook.
If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
import React, { useState, useEffect } from "react";
const UsingHooks = () => {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You've clicked ${count} times.`;
});
return (
// <> is a short syntax for <React.Fragment> and can be used instead of a wrapping div
<>
<p>You've clicked {count} times.</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
);
}
Now that we have useState and hooks the answers are kind of out of date. I came across this question because I was doing something wrong. Here's some simplified code of what I was doing.
// set an initial state
const [ value, setValue ] = useState(0)
// gets called after component is re-rendered
useEffect(() => {
// callback to parent that set props
props.update()
})
// if we have an existing value passed in
if (props.value) {
setValue(props.value)
}
This code was converted from a stateful class to a function using hooks, originally setting the default props in the constructor - but functions don't have constructors and that check happens every time the component re-renders:
calls useState
triggers re-render
useEffect is triggerd
parent is called which sets the props
props update so child renders again
GOTO 1
As you can see this results in an infinite loop. The solution is really quite simple. Here's a mock diff from the original.
- const [ value, setValue ] = useState(0)
+ const [ value, setValue ] = useState(props.value || 0)
- if (props.value) {
- setValue(props.value)
- }
Basically, just initialise the state from the props and don't do silly things like calling useState except in response to an event or callback of some type.
You can use useMemo hook (as below) to demonstrate as constructor for functional component. Somebody suggested to use useEffect but it will be invoked after render.
useMemo(() => {
console.log('This is useMemo')
}, []);
you could set a useState as the first line inside of your functional component and add a function as "initial value":
const MyComponentName = props => {
useState(() => {
console.log('this will run the first time the component renders!');
});
return <div>my component!</div>;
};
You don't. The kind of component in your example is called "stateless functional component". It has no state and no lifecycle methods. If you want your component to be stateful you'll have to write it as a class component.
To simulate constructor in FC use useEffect.
useEffect(() => {
... here your init code
}, []);
That's it! EZ! This useEffect runs only once when the component loads and never runs after, just don't forget to add square brackets at the end.
For those who want to run a function once before the component is mounted, here is a hook (written in TypeScript).
Normally useEffect and useLayoutEffect suffice, but they run after the component is mounted, and sometimes you want to run code before that happens (like a constructor).
import React, { useRef } from "react";
function useOnce<Type>(callBack: () => Type): Type {
const result = useRef<Type | null>(null);
if (result.current !== null) {
return result.current;
}
result.current = callBack();
return result.current;
}
const Component: React.FC<{}> = () => {
const result = useOnce(() => {/* Code you would normally put in a constructor */});
return <div />
}
Alternatively, you can use react-afc
import { afc, reactive } from 'react-afc'
function heavyCalc() {/*...*/}
const Conponent = afc(props => {
// Called once, before the first render
const state = reactive({
name: 'Stack',
inputsCount: 0
})
// Without useMemo(..., [])
const result = heavyCalc()
// The function is created once and does not cause
// a re-render of child components
function onInput(e) {
state.inputsCount++
state.name = e.currentTarget.value
}
// Saved between renders (no longer need useRef)
let rendersCount = 0
// Must return the render-function
return () => {
// The function works like a regular react-component
// Here you can use the usual hooks
rendersCount++
return (
<input onChange={onInput} value={state.name}/>
)
}
})
The package has the necessary methods for working with state (including redux), react-hooks, lifecycle methods and context

Resources