React preserving state even though key has changed - reactjs

The react component is preserving the "wasEdited" state even when the properties and key are changed. When any div element is edited from its initial value, the classname of the div row will be changed, thus showing a red border on the left side.
I was able to get around the issue by resetting the state manually every time the "globalId" property is changed. As you can see in the comment, I suspect this may be a similar issue to this:
React does not re-render updated map, even with different keys
But in my case I'm not mutating an object.
function Attribute ({globalId, name, value}) {
const [wasEdited, setWasEdited] = useState(false);
const handleInput = (e) => {
e.target.innerHTML === value ? setWasEdited(false) : setWasEdited(true);
};
// TODO: This useEffect is a hack to reset the wasEdited state when the globalId changes. I don't understand
// why this is necessary, since each Attribute component is rendered with a unique key. I think it
// has something to do with this post on [stack overflow](https://stackoverflow.com/questions/68949376///react-does-not-re-render-updated-map-even-with-different-keys).
// useEffect(() => {
// return () => setWasEdited(false);
// }, [globalId])
console.log(`${globalId}-${name}`, wasEdited)
return (
<div key={`${globalId}-${name}`} className={wasEdited ? 'row was-edited' : 'row'}>
<div className='col'>{name}</div>
<div className='col' onInput={handleInput} contentEditable>{value}</div>
</div>
)
}
This is what it looks like:

Putting a key on a div will cause that div to unmount and remount when the key changes. But it will have no effect on the component surrounding that div (Attribute). If you want to deliberately unmount and remount Attribute, you need to put a key on Attribute. So wherever you're rendering it, do:
<Attribute key={`${globalId}-${name}`} globalId={globalId} name={name} value={something} />

Related

React: Hook not triggering when parent component changes boolean value passed to child

so I have a parent that is responsible for rendering a list of items. When the item is clicked it will send an event to the parent, that will record on a Map the selection state. If selected it passes the selected state to the child so that it can change style. Here's the code for the parent:
export default function CreateYourMixProductsList(props: ProductsInterface) {
const selectedProducts: Map<number, Product> = new Map<number, Product>();
function onProductClick(product: Product): void {
selectedProducts.set(product.id, product);
}
return (
<>
<List sx={{width: '100%', bgcolor: 'background.paper'}}>
{props?.products?.map(product => (
<ProductListItem key={product.id} product={product}
selected={selectedProducts.has(product.id)}
onClick={(product) => onProductClick(product)} />
))}
</List>
</>
);
and the child
export default function ProductListItem(props: ProductProps) {
const [selected, setSelected] = React.useState(false);
function onClick(product: Product) {
props.onClick(product);
}
useEffect(() => {
setSelected(!selected);
}, [props.selected]);
return (
<>
<ListItemButton alignItems="flex-start" onClick={event => {onClick(props.product)}} selected={props.selected ? selected : false}>
//omitted code to keep it short
The useEffect is triggered only on rendering, whilst to my understanding, it should be triggered every time the props passed down is an immutable variable. What am I missing here?
here's a bit of documentation on useEffect
https://reactjs.org/docs/hooks-effect.html
By default, useEffect will trigger after each render. Adding an array will skip this process and instead only trigger the useEffect on the changing of the specified prop(s). If we take a look at your code example:
useEffect(() => {
setSelected(!selected);
}, [props.selected]);
this block is triggering the useEffect each time props.selected is updated, hence why it only triggers on render, when you give the value of false to selected. What you can do instead is call setSelected(!selected) as part of the onClick event. Once this is occurs, you can update the useEffect to handle any logic that should occur when selected changes, or remove the useEffect entirely.
Lets unwrap this:
Why is the useEffect not running?
Hooks re-run every time a variable in their dependency array changes. Your hook is not running again because the value of props.selected does not change.
You can easily verify this by simply logging the value in the component.
Why is props.selected not changing?
Your click handler correctly sets the value on your Map. However, React does not recognize that a a new value was set inside the map. The component never re-renders and selectedProducts.has() is not called again. So the value of props.selected is indeed still the same.
How can you make React recognize your change?
First of all, you should avoid declaring state like this in your component. Each time this component renders it will re-declare all variables defined inside it (selectedProducts will always be set to a new empty map). Use reacts hook api instead.
To make the variable stick - and reactive - you can simply use it with useState() as you did in your child-component. E.g.:
...
const [selectedProducts, setSelectedProducts] = useState<Map<number, Product>>(new Map<number, Product>());
function onProductClick(product: Product): void {
selectedProducts.set(product.id, product);
// Note how we are re-setting the variable to make React aware of the change!
setSelectedProducts(new Map(selectedProducts));
}
...
It is important to note that Reacts useState hook compares values with tripple-equal (===) to determine whether it should trigger a re-render. Due to the nature of JavaScript (where variables only hold references to objects), setting a new value on a Map will not change its reference. To trigger a re-render you have to replace the Map.
Creating new objects is computationally cheap so this shouldn't cause any issues most of the times.

Is there a way to trigger useState hook when the newvalue is same as oldvalue

I Have an animation to trigger when selectedName changes, but it seems is the newvalue is same as previous value the animation does not trigger. After looking into it, the hook only trigger is newvalue is different than previous value. Is there a way to fix this?
const [selectedName, setSelectedName] = useState("");
useEffect(() => {
console.log("Change", selectedName)
}, [selectedName]);
https://codesandbox.io/s/reactts-text-animation-using-react-transition-group-64gum?file=/src/App.tsx
The idea you had was correct - trigger a re-render on each click even if the number is the same. The only thing you did wrong was assign the same key prop to your CSSTransition element on each render of an identical element.
The key needs to be unique because React uses the key prop to understand the component-to-DOM Element relation, which is then used for the reconciliation process. It is therefore very important that the key always remains unique, otherwise there is a good chance React will mix up the elements and mutate the incorrect one.
Anyway, you do need that second state variable to trigger a rerender. We will do something as basic as: const [trigger, triggerUpdate] = useState<boolean>(false);. You can then tigger it like so: triggerUpdate(!trigger);
And the most important part, of course, is the key prop
return (
<div className="App">
<SwitchTransition mode="out-in">
<CSSTransition<undefined>
classNames="fade"
addEndListener={(node: HTMLElement, done: () => void) => {
node.addEventListener("transitionend", done, false);
}}
key={Math.random()
.toString(36)
.replace(/[^a-z]+/g, "")}
>
<h1 className="randomNumberText">{rndNumber}</h1>
</CSSTransition>
</SwitchTransition>
<button onClick={() => getRandomNumber()}>Click Me!</button>
</div>
);
Notice that I use this mechanism to come up with a unique key, you don't have to follow this example necessarily:
key={Math.random()
.toString(36)
.replace(/[^a-z]+/g, "")}
Sandbox: https://codesandbox.io/s/reactts-text-animation-using-react-transition-group-forked-v2cm4?file=/src/App.tsx

React: why is that changing the current value of ref from useRef doesn't trigger the useEffect here

I have a question about useRef: if I added ref.current into the dependency list of useEffect, and when I changed the value of ref.current, the callback inside of useEffect won't get triggered.
for example:
export default function App() {
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<div className="App">
<button
onClick={() => {
myRef.current = myRef.current + 1;
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
</div>
);
}
Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?
Also I know I can use useState here. This is not what I am asking. And also I know that ref stay referentially the same during re-renders so it doesn't change. But I am not doing something like
const myRef = useRef(1);
useEffect(() => {
//...
}, [myRef]);
I am putting the current value in the dep list so that should be changing.
I know I am a little late, but since you don't seem to have accepted any of the other answers I'd figure I'd give it a shot too, maybe this is the one that helps you.
Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?
Short answer, no.
The only things that cause a re-render in React are the following:
A state change within the component (via the useState or useReducer hooks)
A prop change
A parent render (due to 1. 2. or 3.) if the component is not memoized or otherwise referentially the same (see this question and answer for more info on this rabbit hole)
Let's see what happens in the code example you shared:
export default function App() {
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<div className="App">
<button
onClick={() => {
myRef.current = myRef.current + 1;
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
</div>
);
}
Initial render
myRef gets set to {current: 1}
The effect callback function gets registered
React elements get rendered
React flushes to the DOM (this is the part where you see the result on the screen)
The effect callback function gets executed, "myRef current changed" gets printed in the console
And that's it. None of the above 3 conditions is satisfied, so no more rerenders.
But what happens when you click the button? You run an effect. This effect changes the current value of the ref object, but does not trigger a change that would cause a rerender (any of either 1. 2. or 3.). You can think of refs as part of an "effect". They do not abide by the lifecycle of React components and they do not affect it either.
If the component was to rerender now (say, due to its parent rerendering), the following would happen:
Normal render
myRef gets set to {current: 1} - Set up of refs only happens on initial render, so the line const myRef = useRef(1); has no further effect.
The effect callback function gets registered
React elements get rendered
React flushes to the DOM if necessary
The previous effect's cleanup function gets executed (here there is none)
The effect callback function gets executed, "myRef current changed" gets printed in the console. If you had a console.log(myRef.current) inside the effect callback, you would now see that the printed value would be 2 (or however many times you have pressed the button between the initial render and this render)
All in all, the only way to trigger a re-render due to a ref change (with the ref being either a value or even a ref to a DOM element) is to use a ref callback (as suggested in this answer) and inside that callback store the ref value to a state provided by useState.
https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
use useCallBack instead, here is the explanation from React docs:
We didn’t choose useRef in this example because an object ref doesn’t
notify us about changes to the current ref value. Using a callback ref
ensures that even if a child component displays the measured node
later (e.g. in response to a click), we still get notified about it in
the parent component and can update the measurements.
Note that we pass [] as a dependency array to useCallback. This
ensures that our ref callback doesn’t change between the re-renders,
and so React won’t call it unnecessarily.
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
Ok so I think what you're missing here is that changing a ref's value doesn't cause a re-render. So if it doesn't cause re-renders, then the function doesn't get run again. Which means useEffect isn't run again. Which means it never gets a chance to compare the values. If you trigger a re-render with a state change you will see that the effect will now get run. So try something like this:
export default function App() {
const [x, setX] = useState();
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<button
onClick={() => {
myRef.current = myRef.current + 1;
// Update state too, to trigger a re-render
setX(Math.random());
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
);
}
Now you can see it will trigger the effect.

React: useState value doesn't update on parameter value change

In the example below, the value of the display state on Child component never updates, even if the show parameter toggles between true and false.
I expect it to receive the value and to update accordingly. Can someone please elaborate on why this is not working?
(I know I can use a useEffect callback and setDisplay(show) from inside it, but I'd like to know why a simpler approach like this doesn't work)
function Child({ show }) {
const [display] = React.useState(show);
console.log({ show, display });
return display ? "Message!" : null;
}
function Parent() {
const [show, setShow] = React.useState(false);
const handleClick = () => {
setShow(!show);
};
return (
<div>
<div>
<button onClick={handleClick}>Toggle</button>
</div>
<Child show={show} />
</div>
);
}
Working example: https://codesandbox.io/s/react-boilerplate-4hexp?file=/src/index.js
Well the value of display is set only on the first render of the component (because it is state and state doesnt change with renders, but only when you tell it to change). If you want it to be changing with changing props just use a normal constant instead.
I believe it's because the useState in the Child component is reading show when it first loads but then never updates because it's just set, it doesn't automatically update.
You could either just use show directly which should be used for return show ? 'message' : <></>
Or you could still use the local state with useState, but you would need to add a useEffect to listen to the props change then change the local state of that child.
Update:
Third option for your current code to work would also be to do:
{show && <Child show={show} />}
That way at the time when it's true, the component will read the latest data.
display is local component state of Child, given an initial value from props.show when Child mounted. There is never a state update within Child to render any other value of display. This is actually an anti-pattern to store passed props in local component state, but there are two alternatives/solutions to getting display to update.
Use an effect to update state when the props update
function Child({ show }) {
const [display, setDisplay] = React.useState(show);
useEffect(() => setDisplay(show), [show]);
console.log(show, display);
return display ? "Message!" : null;
}
Or better, just consume the prop show directly
function Child({ show }) {
console.log(show);
return show ? "Message!" : null;
}
The benefit of the latter is that the new value of show and the updated/rerendered UI occur in the same render cycle. With the former (the anti-pattern) the state needs to update then the component rerenders, so the updated UI is a render cycle delayed.

useEffect not triggering

I think my useEffect is broken.
I have on the parent component
<div className={redactKategori}>
<select className="form-control form-control-sm mb-3 mr-2" name="betalt" defaultValue={'välj kategori'} onChange={e => { e.preventDefault(); setKate(e.target.value); setRedactKategoris('d-block') }}>
<option >valj kategori</option>
{ Kategori ?
Object.keys(Kategori).map(key => {
return (
<option key={key} value={key}>{key}</option>
)
}) :
null
}
</select>
<div className={redactKategoris}>
<EditKat aa={kate} />
</div>
and on the child component
function EditKat({ aa }) {
let defaultValues = 0
useEffect(() => {
defaultValues = 2
}, [aa])
console.log(defaultValues)
}
So, as far as I understood, everytime ${kate} would get a value on the parent component, the child component would trigger the useEffect hook. However it is not working. Am I doing something wrong?
The reason for the experienced behavior is not that useEffect isn't working. It's because of the way function components work.
If you look at your child component, if useEffect is executed and the component rerenders, defaultValues would be set to 0 again, because the code inside of the function is executed on each render cycle.
To work around that, you would need to use the useState to keep your local state consistent across renders.
This would look something like this:
function EditKat({ aa }) {
// Data stored in useState is kept across render cycles
let [defaultValues, setDefaultValues] = useState(0)
useEffect(() => {
setDefaultValues(2) // setDefaultValues will trigger a re-render
}, [aa])
console.log(defaultValues)
}
Try adding a key prop on your component when it is created in the parent code
<yourcomponent key="uniquevalue" />
This is because in most cases, when your component is reused, based on the way it is created, it may usually re-render it with some changes instead of recreating it again when you reuse it, Hence the useEffect is not going to be called. eg in SwitchRoute, loops, conditionals...
So adding a key will prevent this from happening. If it is in a loop make sure each element is unique, maybe by including the index i in the key if you can't find any better unique key.
I faced the same problem, I debugged it and i found that, i am mutating the state directly instead of cloning it and using it. So, that's why useEffect is not triggered.

Resources