Hooks parent unmounted before children - reactjs

I' working on react since few months. I started Hooks since few days (I know quite late) the thing is, compare to react component the life cycle methodes it's look like they are different on some points.
The useEffect hook can reproduce :
-componentDidMount();
-componentDidUpdate();
-componentWillUnMount();
But I observe a difference between react's component and function it's about the way how function is unmounted. I noted the unmount methode, compare to the react's component,the react's function unmount the parent before the child/ren
import React, { ReactElement, useEffect, useState } from "react";
import { useLocation, useHistory } from "react-router-dom";
export function Child2({
count,
childrenUnmounted,
}: {
count: number;
childrenUnmounted: Function;
}): ReactElement {
useEffect(() => {
return () => {
console.log("Unmounted");
childrenUnmounted(count);
};
}, [, count]);
return (
<div>
<h2>Unmouted</h2>
</div>
);
}
export function Child1({ count }: { count: number }): ReactElement {
const [validation, setValidation] = useState(false);
const usehistory = useHistory();
const childrenUnmounted = (count: number) => {
console.log("validation", validation, count);
setValidation(false);
};
const changeUrl = () => {
setValidation(true);
usehistory.push("http://localhost:3000/${count}");
};
return (
<div>
<h2>incremente</h2>
<Child2
count={count}
childrenUnmounted={(count: number) => childrenUnmounted(count)}
/>
<button className="button" onClick={() => changeUrl()}>
validation
</button>
<button
className="button"
onClick={() => usehistory.push(`http://localhost:3000/${count}`)}
>
nope
</button>
</div>
);
}
export default function Parent(): ReactElement {
const [count, setcount] = useState(-1);
const location = useLocation();
useEffect(() => {
setcount(count + 1);
}, [, location]);
return (
<div>
<h2>hello</h2>
<h3>{count}</h3>
<Child1 count={count} />
</div>
);
}
With the code above something annoying happen, when you clicked on the validation button. Value in the Child1is at true, at the moment of the click, and it's change the URL to trigger a rerender of the Parent to change the data (here count).
The thing I don't understand is why at the unmount of the Child2, at the childrenUnmounted(count) called (to trigger the same function but in the Child1) in the Child1 the validation is equal to false even the validation was clicked ? and when you click on nope just after validation you got true... it's look like the Child1 do not matter of the current state of the validation (he use the previous state)
Someone could help me to understand what's going on ?
Thx of the help.
SOLUTION:
I used useRef instead of useState from the validation to don't depend of the re-render as Giovanni Esposito said :
because hooks are async and you could not get the last value setted for state
So useRef was my solution

Ciao, I think you problem is related on when you logs validation value. I explain better.
Your parent relationship are: Parent -> Child1 -> Child2. Ok.
Now you click validation button on Child2. validation button calls changeUrl that calls usehistory.push("http://localhost:3000/${count}"); and starts to change validation value (why starts? because setValidation is async).
If the unmounting of Child2 comes now, could be that validation value is no yet setted by async setValidation (and log returns the old value for validation).
Well, at some point this setValidation finished and sets validation to true. Now you click nope button and you get true for validation (the last value setted).
So, to make the story short, I think that what you are seeing in logs it's just because hooks are async and you could not get the last value setted for state (if you use log in this way). The only way you have to log always the last value setted is useEffect hook with value you want to log in deps list.

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.

Why React is rendering parent element, even if changed state isn't used in jsx? (Using React Hooks)

Im doing a React small training app using Hooks. Here's the example:
There is a MainPage.js and it has 3 similar child components Card.js. I have global state in MainPage and each Card has its own local state. Every Card has prop "id" from MainPage and clickButton func.
When I click button in any Card there are 2 operations:
Local variable 'clicked' becomes true.
The function from parent component is invoked and sets value to global state variable 'firstCard'.
Each file contains console.log() for testing. And when I click the button it shows actual global variable "firstCard", and 3x times false(default value of variable "clicked" in Card).
It means that component MainPage is rendered after clicking button ? And every Card is rendered too with default value of "clicked".
Why MainPage componenet is rendered, after all we dont use variable "firsCard", except console.log()?
How to make that after clicking any button, there will be changes in exactly component local state, and in the same time make global state variable "firstCard" changed too, but without render parent component(we dont use in jsx variable "firstCard")
Thanks for your help !
import Card from "../Card/Card";
const Main = () => {
const [cards, setCards] = useState([]);
const [firstCard, setFirstCard] = useState(null);
useEffect(() => {
setCards([1, 2, 3]);
}, []);
const onClickHandler = (id) => {
setFirstCard(id);
};
console.log(firstCard); // Showing corrrect result
return (
<div>
{cards.map((card, i) => {
return (
<Card
key={Date.now() + i}
id={card}
clickButton={(id) => onClickHandler(id)}
></Card>
);
})}
</div>
);
};
import React, { useState } from "react";
const Card = ({ id, clickButton }) => {
const [clicked, setClicked] = useState(false);
const onClickHandler = () => {
setClicked(true);
clickButton(id);
};
console.log(clicked); // 3x false
return (
<div>
<h1>Card number {id}</h1>
<button onClick={() => onClickHandler()}> Set ID</button>
</div>
);
};
export default Card;
You have wrong idea how react works.
When you change something in state that component will re render, regardless if you use that state variable in render or not.
Moreover, react will also re render all children of this component recursively.
Now you can prevent the children from re rendering (not the actual component where state update happened though) in some cases, for that you can look into React.memo.
That said prior to React hooks there was a method shouldComponentUpdate which you could have used to skip render depending on change in state or props.

Prevent useState value from being reset when props change

I have a component that looks something like this:
//#flow
import React, { useState } from "react";
type Props = {
likes: int,
toggleLike: () => void,
};
const Foo = (props: Props) => {
const [open, setOpen] = useState(false);
const style = `item${open ? " open": ""}`;
return (
<div className={style} onMouseOver={() => setOpen(true)} onFocus={() => setOpen(true)} onMouseOut={() => setOpen(false)} onBlur={() => setOpen(false)}>
<button onClick={props.toggleLike}>Toggle like</button>
</div>
);
};
export default Foo;
The open state is used to apply the "open" class when moused over. The problem comes if I call the toggleLike() prop function, since this updates the props and the component is rerendered with open reset to false. As the style uses a transition, this results in the animation rerunning as it changes back to false, then to true due to the mouse being over it.
So, how can I prevent open being reset back to false on each subsequent render? It seems like it should be straightforward, but after going through https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state I can't seem to apply it in my case.
State does not reset when props change. State is on a per component basis and is preserved throughout re-renders, hence being called "state".
As Dennis Vash already mentioned, the problem is most likely caused by the component being unmounted or replaced by an identical component. You can verify this easily by adding this to your component:
useEffect(() => {
console.log("Mounted")
}, [])
You should see multiple "Mounted" in the console.
If there's no way to prevent the component from being replaced or unmounted, consider putting the state into a context and consume that context inside your component, as you can also wrap each of your components into its own context to give it a unique, non-global, state.

What invokves the 2nd function call when using React Hooks?

I wrote the following React exercise which uses no hooks and renders a button.
const Button = ({ onClick }) => <button onClick={onClick}>Do Nothing</button>;
const Base = () => {
const onClickFunction = (() => {
console.log("Creating OnClick Function");
return () => {};
})();
return (
<div className="App">
<h1>Hello</h1>
<Button onClick={onClickFunction} />
</div>
);
};
onClickFunction uses a self-invoking function, so that I can place a console.log to see the following behaviour. In this example, when Base is rendered, the message Creating OnClick Function appears only once 👍
If I change Base to the following however, adding a hook usage:
const Button = ({ onClick }) => <button onClick={onClick}>Do Nothing</button>;
const Base = () => {
const notUsedRef = React.useRef();
const onClickFunction = (() => {
console.log("Creating OnClick Function");
return () => {};
})();
return (
<div className="App">
<h1>Hello</h1>
<Button onClick={onClickFunction} />
</div>
);
};
You will see the Creating OnClick Function message twice.
This CodeSandbox illustrates what I've been seeing: https://codesandbox.io/s/dawn-forest-99clo?file=/src/App.js
Using React DevTools Profiler, we can see there is no rerender of this component.
Using <React.Profiler, it reports this component also didn't update.
I know that using React.useCallback wouldn't trigger a second invokation, however the question would still stand why we are in the situation Base is called twice.
My question is: why and what is triggering Base to be invoked when there is no need for a rerender.
This is due to the way React implements hooks.
If you invoke any hook, even if you don't use the resulting value, you are telling React to render twice before mounting, even if the props don't change. You can substitute the usage of useRef by useState, useEffect, etc. Try below.
You can also wrap your component with React.memo. Every function defined inside the function is recreated in every render.
https://codesandbox.io/s/elastic-water-y18w0?file=/src/App.js
EDIT: Only happens during development and in components wrapped by React.StrictMode. In the words of gaearon:
It's an intentional feature of the StrictMode. This only happens in
development, and helps find accidental side effects put into the
render phase. We only do this for components with Hooks because those
are more likely to accidentally have side effects in the wrong place.
https://github.com/facebook/react/issues/15074

How to Unmount React Functional Component?

I've built several modals as React functional components. They were shown/hidden via an isModalOpen boolean property in the modal's associated Context. This has worked great.
Now, for various reasons, a colleague needs me to refactor this code and instead control the visibility of the modal at one level higher. Here's some sample code:
import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import { UsersProvider } from '../../../contexts/UsersContext';
import AddUsers from './AddUsers';
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
export default AddUsersLauncher;
This all works great initially. A button is rendered and when that button is pressed then the modal is shown.
The problem lies with how to hide it. Before I was just setting isModalOpen to false in the reducer.
When I had a quick conversation with my colleague earlier today, he said that the code above would work and I wouldn't have to pass anything into AddUsers. I'm thinking though that I need to pass the setShowModal function into the component as it could then be called to hide the modal.
But I'm open to the possibility that I'm not seeing a much simpler way to do this. Might there be?
To call something on unmount you can use useEffect. Whatever you return in the useEffect, that will be called on unmount. For example, in your case
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
useEffect(() => {
return () => {
// Your code you want to run on unmount.
};
}, []);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
Second argument of the useEffect accepts an array, which diff the value of elements to check whether to call useEffect again. Here, I passed empty array [], so, it will call useEffect only once.
If you have passed something else, lets say, showModal in the array, then whenever showModal value will change, useEffect will call, and will call the returned function if specified.
If you want to leave showModal as state variable in AddUsersLauncher and change it from within AddUsers, then yes, you have to pass the reference of setShowModal to AddUsers. State management in React can become messy in two-way data flows, so I would advise you to have a look at Redux for storing and changing state shared by multiple components

Resources