Prevent useState value from being reset when props change - reactjs

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.

Related

In react world, when does props changing happen? It's definitely triggered by parent component re-rendering?

When it comes to thinking about possibility of props changing, should I only care about parent component updating? Any other possible cases exist for props changing?
I can guess only one case like the code below. I would like to know if other cases exist.
const ParentComponent = () => {
const [message, setMessage] = useState('Hello World');
return (
<>
<button onClick={() => setMessage('Hello Foo')}>Click</button>
<ChildComponent message={message} />
</>
);
};
const ChildComponent = ({ message }: { message: string }) => {
return <div>{message}</div>;
};
Props can change when a component's parent renders the component again with different properties. I think this is mostly an optimization so that no new component needs to be instantiated.
Much has changed with hooks, e.g. componentWillReceiveProps turned into useEffect+useRef (as shown in this other SO answer), but Props are still Read-Only, so only the caller method should update it.

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.

Hooks parent unmounted before children

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.

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

Why is my component rendering when useState is called with the same state?

I have a simple functional component with a boolean state. And buttons to change the state.
It is initially set to true so when I press the true-button, it does NOT render.
But if I press the false-button, it re-renders AND if I press false-button AGAIN, it will re-render even though the state is already set to false..
Could someone explain why the component re-renders when the state changes to the exact same state? How to prevent it from re-rendering?
import React, {useState} from 'react';
const TestHooks = () => {
const [state, setState] = useState(true);
console.log("rendering..", state);
return(
<div>
<h1>{state.toString()}</h1>
<button onClick={() => setState(true)}>true</button>
<button onClick={() => setState(false)}>false</button>
</div>
)
}
export default TestHooks;
From the react docs :
If you update a State Hook to the same value as the current state,
React will bail out without rendering the children or firing effects.
(React uses the Object.is comparison algorithm.)
Note that React may still need to render that specific component again
before bailing out. That shouldn’t be a concern because React won’t
unnecessarily go “deeper” into the tree. If you’re doing expensive
calculations while rendering, you can optimize them with useMemo.
So your component will not re-render every time setState is called with the same state. It will re-render only 2 times and then bail out. That's just how react works and that should not be a concern except if you're doing some heavy calculations in the render method.
It doesn't matter if the state value is already the same as the one you are trying to update it with. The fact is, setState() will re-render your component regardless, that's just what React does to ensure all changes (if any) are reflected in the UI.
If you want to avoid unnecessary re-renders, use an intermediary function to check if the state values are already equal before updating it.
import React, {useState} from 'react';
const TestHooks = () => {
const [state, setState] = useState(true);
const updateState = (boolean) => {
if(boolean !== state){
setState(boolean)
}
}
console.log("rendering..", state);
return(
<div>
<h1>{state.toString()}</h1>
<button onClick={() => updateState(true)}>true</button>
<button onClick={() => updateState(false)}>false</button>
</div>
)
}
export default TestHooks;
render() will be called whenever setState() is called. That is the reason why we have the concept of PureComponent in React. Read https://reactjs.org/docs/react-api.html#reactpurecomponent

Resources