React hooks to update state based on the previous state value [duplicate] - javascript

This question already has answers here:
Why can't I directly modify a component's state, really?
(7 answers)
Closed 2 years ago.
Using hooks to update state based on the previous state value, I don't understand why modifying the existing object and passing that to setState() is bad. I know this won't cause a re-render since the state still points to the same reference, but beyond that, what is the issue? I don't understand how cloning the array, modifying that, then passing it to setState() fixes some unknown issue.
const [bigArr, setBigArr] = setState(Array(SOME_BIG_NUMBER).fill(false));
// (1) This seems to work, but is bad for some reason. But why?
bigArr[325] = true;
setBigArr(bigArr);
// (2) This is preferable for some reason. Why?
bigArrCopy = bigArr.slice();
bigArrCopy[325] = true;
setBigArr(bigArrCopy);
// (3) Is this OK? Why/Why not?
setBigArr(bigArrCopy => {
bigArrCopy[325] = true;
return bigArrCopy;
});

I know this won't cause a re-render since the state still points to the same reference, but beyond that, what is the issue?
Is that not enough? The reason to set state is because you want the component to rerender. If you're trying to get it to rerender and it doesn't, that's a pretty serious bug.
The underlying reason why react went with a model of immutable state is that it makes it very simple to tell whether state changed. Do a quick === between two states, and you immediately know whether it has changed. If you mutate your state, this feature is lost, and any code that depends on it breaks.

The first case will not work as expected, it will not re-render the component because React use shallow comparison which means that it will compare location of object and array, if the location not change React will not trigger re-render
// (1) This will not re-render the component, cuz the location of bigArr are not changed
bigArr[325] = true;
setBigArr(bigArr);
Happen the same with third case
You may say you can fix it by call setBigArr([...bigArr]);. But there is still problems
This is an simple situation you get unexpected result
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [arr, setArr] = useState([1]);
const handleClick = () => {
createTimeout()
// Case1: directly set state
arr[0]= 2
setArr(arr);
// Case1: Not directly set state
// const newArr = [...arr]
// newArr[0]= 2
// setArr(newArr);
};
const createTimeout = () =>{
setTimeout(() =>{
// You expect number 1 here because you call this before update state
console.log(arr[0])
},2000)
}
return (
<div className="App">
<h1>{arr[0]}</h1>
<div onClick={handleClick}>change</div>
</div>
);
}
We call createTimeout before setState so we will expect number 1 will be logged but:
Case 1: you will get number 2 because you mutated original array
Case 2: you will get number 1 (expected)
Check out the codesandbox

React checks to see if the bigArrState !== prevBigArrState before re-rendering. It does not check the contents of the array. It checks if it's the same instance. In your first example that will result in false, and the component will not re-render. When you use bigArr.slice(), you are creating an entire new array therefore bigArrState !== prevBigArrState results in true, allowing the component to re-render.
Your last example will causes issues because the updater func does not get passed bigArrCopy but rather bigArrState (same instance).
https://reactjs.org/docs/react-component.html#setstate
Rather than creating and storing an entire clone in memory you can do the following:
setBigArr([
...bigArr.slice(0, 325),
true,
...bigArr.slice(326),
]);

Related

React/Next-js : Getting TypeError: Cannot read properties of undefined (reading 'id'), but the object is clearly not empty?

I am having an issue with a Next-js React checkbox list snippet after extracting it into the sandbox.
whenever I clicked the checkbox, I get the error:
TypeError: Cannot read properties of undefined (reading 'id')
which originated from line 264:
setCheckedThread(prev => new Set(prev.add(pageData.currentThreads[index].id)));
but at the top of the index.js I have defined the static JSON
and in useEffect() I update the pageData state with:
setPageData({
currentPage: threadsDataJSON.threads.current_page,
currentThreads: threadsDataJSON.threads.data,
totalPages: totalPages,
totalThreads: threadsDataJSON.threads.total,
});
so why when I clicked the checkbox it throws the error?
my sandbox link: https://codesandbox.io/s/infallible-goldberg-vfu0ve?file=/pages/index.js
It looks like your useEffect on line 280 only triggers once you've checked a box (for some reason), so until you trigger that useEffect, pageData.currentThreads remains empty, which is where the error you're running into comes from.
I'd suggest moving all the state initialization from the useEffect into the useState call itself. E.g.
// Bad
const [something, setSomething] = useState(/* fake initial state */);
useEffect(() => {
setSomething(/* real initial state */)
}, []);
// Good
const [something, setSomething] = useState(/* real initial state */);
Here's a fork of your sandbox with this fix.
This is occurring because in Home you've created the handleOnChange function which is passed to the List component that is then passed to the memoized Item component. The Item component is kept the same across renders (and not rerendered) if the below function that you've written returns true:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked
);
}
This means that the Item component holds the first initial version of handleOnChange function that was created when Home first rendered. This version of hanldeOnChange only knows about the initial state of pageData as it has a closure over the initial pageData state, which is not the most up-to-date state value. You can either not memoize your Item component, or you can change your itemPropsAreEqual so that Item is rerendered when your props.handleOnChange changes:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked &&
prevItem.handleOnChange === nextItem.handleOnChange // also rerender if `handleOnChange` changes.
);
}
At this point you're checking every prop passed to Item in the comparison function, so you don't need it anymore and can just use React.memo(Item). However, either changing itemPropsAreEqual alone or removing itemPropsAreEqual from the React.memo() call now defeats the purpose of memoizing your Item component as handleOnChange gets recreated every time Home rerenders (ie: gets called). This means the above check with the new comparison function will always return false, causing Item to rerender each time the parent Home component rerenders. To manage that, you can memoize handleOnChange in the Home component by wrapping it in a useCallback() hook, and passing through the dependencies that it uses:
const handleOnChange = useCallback(
(iindex, id) => {
... your code in handleOnChange function ...
}
, [checkedState, pageData]); // dependencies for when a "new" version of `handleOnChange` should be created
This way, a new handleOnChange reference is only created when needed, causing your Item component to rerender to use the new up-to-date handleOnChange function. There is also the useEvent() hook which is an experimental API feature that you could look at using instead of useCallback() (that way Item doesn't need to rerender to deal with handleOnChange), but that isn't available yet as of writing this (you could use it as a custom hook for the time being though by creating a shim or using alternative solutions).
See working example here.

Why is React state not updated inside functions? [duplicate]

This question already has answers here:
React - useState - why setTimeout function does not have latest state value?
(2 answers)
Closed 7 months ago.
I have a component that renders a table with objects. This component shows a button that, when pressed, sends the parent a specific object. The parent must set it in the state to display some graphical stuff. The rendering is working correctly, what I don't understand is why I am getting an outdated value after setting the state correctly.
It's not a race condition, React is simply ignoring the updated value of a variable, even when it re-renders the component correctly.
A minimal example:
import { useState } from "react";
import { SomeComponent } from "./SomeComponent";
export default function App() {
const [currentID, setCurrentID] = useState(null);
function getData() {
console.log("Getting data of: ", currentID); // PROBLEM: this is null
}
function setAndRetrieveData(value) {
setCurrentID(value);
// Just to show the problem and discard race conditions.
setTimeout(() => {
getData();
}, 1500);
}
return (
<div className="App">
<h1>Current ID: {currentID}</h1> {/* This works fine */}
<SomeComponent getInfoFor={setAndRetrieveData} />
</div>
);
}
SomeComponent:
export function SomeComponent(props) {
const randomID = 45;
return <button onClick={() => props.getInfoFor(randomID)}>Get info</button>;
}
Even with solutions like useStateCallback the problem persists.
Is there a way to do this without having to use the awful useEffect which is not clear when reading the code? Because the logic of the system is "when this button is pressed, make a request to obtain the information", using the hook useEffect the logic becomes "when the value of currentID changes make a request", if at some point I want to change the state of that variable and perform another action that is not to obtain the data from the server then I will be in trouble.
Thanks in advance
I think this is an issue with the way Javascript closures work.
When you execute a function, it gets bundled with all the data that pertains to it and then gets executed.
The issue is that you call this:
setTimeout(() => {
getData();
}, 1500);
inside setAndRetrieveData(value).
Even though it's inside a setTimeout, the getData() function has been bundled with the information it needs (currentID) at that point in time, not when it actually runs. So it gets bundled with the currentId before the state update takes place
Unfortunately, I would recommend using useEffect. This is the best way to ensure you avoid issues like this and any potential race conditions. Hopefully someone else can provide a different approach!
when setAndRetrieveData is called it sets a state that leads to the component being rerendered to reflect the new state. When the timeout finishes The function getData was created in the previous render. And thus only has access to the state variable from the previous render. That now is undefined.
what you could try is using a useEffect hook that that listens to changes of
currentID.
useEffect(() => {
const timeoutId = setTimeout(() => {
// Do something with the updated value
},1000);
return () => {
// if the data updates prematurely
// we cancel the timeout and start a new one
clearTimeout(timeoutId);
}
},[currentID])

React prevent re-render by using functional variant of useState

I am trying to understand the exact difference in terms of how the re-render of function component is caused in one case using plain setState V/s other case which uses functional state update
The relevant code snippet is as below
Case 1 : Causes re-render of the component
const onRemove = useCallback(
tickerToRemove => {
setWatchlist(watchlist.filter(ticker => ticker !== tickerToRemove));
},
[watchlist]
);
Case 2 : Does not cause re-render
const onRemove = useCallback(tickerToRemove => {
setWatchlist(watchlist =>
watchlist.filter(ticker => ticker !== tickerToRemove)
);
}, []);
Full example of both the use-cases can be seen on;
https://codesandbox.io/s/06c-usecallback-final-no-rerenders-bsm64?file=/src/watchlistComponent.js
https://codesandbox.io/s/06b-usecallback-usememo-ngwev?file=/src/watchlistComponent.js:961-970
UPDATE
Full article link
https://medium.com/#guptagaruda/react-hooks-understanding-component-re-renders-9708ddee9928#204b
I am a bit confused as to how the re-render of child components is prevented.
In the article it says
"Thankfully, setter function from useState hook supports a functional
variant which comes to our rescue. Instead of calling setWatchlist
with the updated watchlist array, we can instead send a function that
gets the current state as an argument"
However, I am a bit confused whether the re-rendering of child components is prevented because we use empty array (as [] does not changes between renders) V/s prevented because of using setter variant of useState hook ?
Using a functional state update or not is rather irrelevant to the question you are asking about. You appear to be asking why (1) a callback with dependency triggers a rerender versus (2) a callback with empty dependency.
The answer is quite literally very simple. In version (2) you are providing a stable callback reference from the time the component mounts that never changes, whereas in (1) the callback reference changes when the dependency does. Remember that React components rerender when state or props update (a new callback reference is a new prop reference) or when the parent component rerenders. Since the onRemove prop is updating in (1) it triggers a rerender.

Props updates in setInterval not reflected when changing component [duplicate]

Why in the following pseudo-code example Child doesn't re-render when Container changes foo.bar?
Container {
handleEvent() {
this.props.foo.bar = 123
},
render() {
return <Child bar={this.props.foo.bar} />
}
Child {
render() {
return <div>{this.props.bar}</div>
}
}
Even if I call forceUpdate() after modifying the value in Container, Child still shows the old value.
Update the child to have the attribute 'key' equal to the name. The component will re-render every time the key changes.
Child {
render() {
return <div key={this.props.bar}>{this.props.bar}</div>
}
}
Because children do not rerender if the props of the parent change, but if its STATE changes :)
What you are showing is this:
https://facebook.github.io/react/tips/communicate-between-components.html
It will pass data from parent to child through props but there is no rerender logic there.
You need to set some state to the parent then rerender the child on parent change state.
This could help.
https://facebook.github.io/react/tips/expose-component-functions.html
I had the same problem.
This is my solution, I'm not sure that is the good practice, tell me if not:
state = {
value: this.props.value
};
componentDidUpdate(prevProps) {
if(prevProps.value !== this.props.value) {
this.setState({value: this.props.value});
}
}
UPD: Now you can do the same thing using React Hooks:
(only if component is a function)
const [value, setValue] = useState(propName);
// This will launch only if propName value has chaged.
useEffect(() => { setValue(propName) }, [propName]);
When create React components from functions and useState.
const [drawerState, setDrawerState] = useState(false);
const toggleDrawer = () => {
// attempting to trigger re-render
setDrawerState(!drawerState);
};
This does not work
<Drawer
drawerState={drawerState}
toggleDrawer={toggleDrawer}
/>
This does work (adding key)
<Drawer
drawerState={drawerState}
key={drawerState}
toggleDrawer={toggleDrawer}
/>
Confirmed, adding a Key works. I went through the docs to try and understand why.
React wants to be efficient when creating child components. It won't render a new component if it's the same as another child, which makes the page load faster.
Adding a Key forces React to render a new component, thus resetting State for that new component.
https://reactjs.org/docs/reconciliation.html#recursing-on-children
Obey immutability
Quite an old question but it's an evergreen problem and it doesn't get better if there are only wrong answers and workarounds.
The reason why the child object is not updating is not a missing key or a missing state, the reason is that you don't obey the principle of immutability.
It is the aim of react to make apps faster and more responsive and easier to maintain and so on but you have to follow some principles. React nodes are only rerendered if it is necessary, i.e. if they have updated. How does react know if a component has updated? Because it state has changed. Now don't mix this up with the setState hook. State is a concept and every component has its state. State is the look or behaviour of the component at a given point in time. If you have a static component you only have one state all the time and don't have to take care of it. If the component has to change somehow its state is changing.
Now react is very descriptive. The state of a component can be derived from some changing information and this information can be stored outside of the component or inside. If the information is stored inside than this is some information the component has to keep track itself and we normally use the hooks like setState to manage this information. If this information is stored outside of our component then it is stored inside of a different component and that one has to keep track of it, its theirs state. Now the other component can pass us their state thru the props.
That means react rerenders if our own managed state changes or if the information coming in via props changes. That is the natural behaviour and you don't have to transfer props data into your own state.
Now comes the very important point: how does react know when information has changed? Easy: is makes an comparison! Whenever you set some state or give react some information it has to consider, it compares the newly given information with the actually stored information and if they are not the same, react will rerender all dependencies of that information.
Not the same in that aspect means a javascript === operator.
Maybe you got the point already.
Let's look at this:
let a = 42;
let b = a;
console.log('is a the same as b?',a === b); // a and b are the same, right? --> true
a += 5; // now let's change a
console.log('is a still the same as b?',a === b); // --> false
We are creating an instance of a value, then create another instance, assign the value of the first instance to the second instance and then change the first instance.
Now let's look at the same flow with objects:
let a = { num: 42};
let b = a;
console.log('is a the same as b?',a === b); // a and b are the same, right? --> true
a.num += 5; // now let's change a
console.log('is a still the same as b?',a === b); // --> true
The difference this time is that an object actually is a pointer to a memory area and with the assertion of b=a you set b to the same pointer as a leading to exactly the same object.
Whatever you do in a can be accesed by your a pointer or your b pointer.
Your line:
this.props.foo.bar = 123
actually updates a value in the memory where "this" is pointing at.
React simply can't recognize such alterations by comparing the object references. You can change the contents of your object a thousand times and the reference will always stay the same and react won't do a rerender of the dependent components.
That is why you have to consider all variables in react as immutable. To make a detectable change you need a different reference and you only get that with a new object. So instead of changing your object you have to copy it to a new one and then you can change some values in it before you hand it over to react.
Look:
let a = {num: 42};
console.log('a looks like', a);
let b = {...a};
console.log('b looks like', b);
console.log('is a the same as b?', a === b); // --> false
The spread operator (the one with the three dots) or the map-function are common ways to copy data to a new object or array.
If you obey immutability all child nodes update with new props data.
According to React philosophy component can't change its props. they should be received from the parent and should be immutable. Only parent can change the props of its children.
nice explanation on state vs props
also, read this thread Why can't I update props in react.js?
You should use setState function. If not, state won't save your change, no matter how you use forceUpdate.
Container {
handleEvent= () => { // use arrow function
//this.props.foo.bar = 123
//You should use setState to set value like this:
this.setState({foo: {bar: 123}});
};
render() {
return <Child bar={this.state.foo.bar} />
}
Child {
render() {
return <div>{this.props.bar}</div>
}
}
}
Your code seems not valid. I can not test this code.
You must have used dynamic component.
In this code snippet we are rendering child component multiple time and also passing key.
If we render a component dynamically multiple time then React doesn't render that component until it's key gets changed.
If we change checked by using setState method. It won't be reflected in Child component until we change its key. We have to save that on child's state and then change it to render child accordingly.
class Parent extends Component {
state = {
checked: true
}
render() {
return (
<div className="parent">
{
[1, 2, 3].map(
n => <div key={n}>
<Child isChecked={this.state.checked} />
</div>
)
}
</div>
);
}
}
My case involved having multiple properties on the props object, and the need to re-render the Child on changing any of them.
The solutions offered above were working, yet adding a key to each an every one of them became tedious and dirty (imagine having 15...). If anyone is facing this - you might find it useful to stringify the props object:
<Child
key={JSON.stringify(props)}
/>
This way every change on each one of the properties on props triggers a re-render of the Child component.
Hope that helped someone.
I have the same issue's re-rendering object props, if the props is an object JSON.stringify(obj) and set it as a key for Functional Components. Setting just an id on key for react hooks doesn't work for me. It's weird that to update the component's you have to include all the object properties on the key and concatenate it there.
function Child(props) {
const [thing, setThing] = useState(props.something)
return (
<>
<div>{thing.a}</div>
<div>{thing.b}</div>
</>
)
}
...
function Caller() {
const thing = [{a: 1, b: 2}, {a: 3, b: 4}]
thing.map(t => (
<Child key={JSON.stringify(t)} something={thing} />
))
}
Now anytime the thing object changes it's values on runtime, Child component will re-render it correctly.
You should probably make the Child as functional component if it does not maintain any state and simply renders the props and then call it from the parent. Alternative to this is that you can use hooks with the functional component (useState) which will cause stateless component to re-render.
Also you should not alter the propas as they are immutable. Maintain state of the component.
Child = ({bar}) => (bar);
export default function DataTable({ col, row }) {
const [datatable, setDatatable] = React.useState({});
useEffect(() => {
setDatatable({
columns: col,
rows: row,
});
/// do any thing else
}, [row]);
return (
<MDBDataTableV5
hover
entriesOptions={[5, 20, 25]}
entries={5}
pagesAmount={4}
data={datatable}
/>
);
}
this example use useEffect to change state when props change.
I was encountering the same problem.
I had a Tooltip component that was receiving showTooltip prop, that I was updating on Parent component based on an if condition, it was getting updated in Parent component but Tooltip component was not rendering.
const Parent = () => {
let showTooltip = false;
if(....){ showTooltip = true; }
return(
<Tooltip showTooltip={showTooltip}></Tooltip>
)
}
The mistake I was doing is to declare showTooltip as a let.
I realized what I was doing wrong I was violating the principles of how rendering works, Replacing it with hooks did the job.
const [showTooltip, setShowTooltip] = React.useState<boolean>(false);
define changed props in mapStateToProps of connect method in child component.
function mapStateToProps(state) {
return {
chanelList: state.messaging.chanelList,
};
}
export default connect(mapStateToProps)(ChannelItem);
In my case, channelList's channel is updated so I added chanelList in mapStateToProps
In my case I was updating a loading state that was passed down to a component. Within the Button the props.loading was coming through as expected (switching from false to true) but the ternary that showed a spinner wasn't updating.
I tried adding a key, adding a state that updated with useEffect() etc but none of the other answers worked.
What worked for me was changing this:
setLoading(true);
handleOtherCPUHeavyCode();
To this:
setLoading(true);
setTimeout(() => { handleOtherCPUHeavyCode() }, 1)
I assume it's because the process in handleOtherCPUHeavyCode is pretty heavy and intensive so the app freezes for a second or so. Adding the 1ms timeout allows the loading boolean to update and then the heavy code function can do it's work.
You can use componentWillReceiveProps:
componentWillReceiveProps({bar}) {
this.setState({...this.state, bar})
}
Credit to Josh Lunsford
Considering the rendering limitations with props and the gains we have with states, if you use reaction hooks, there are a few tricks you can use. For example, you can convert props to state manually using useEffect. It probably shouldn't be the best practice, but it helps in theses cases.
import { isEqual } from 'lodash';
import { useEffect, useState } from 'react';
export const MyComponent = (props: { users: [] }) => {
const [usersState, setUsersState] = useState([]);
useEffect(() => {
if (!isEqual(props.users, usersState)) {
setUsersState(props.users);
}
}, [props.users]);
<OtherComponent users={usersState} />;
};

Calling setState without triggering re-render

I am storing a UI state in the React component's state, say this.state.receivedElements which is an array. I want re-renders whenever an element is pushed to receivedElements. My question is, can I not trigger rendering when the array becomes empty ?
Or in general, can I call setState() just one time without re-render while re-rendering all other times ? ( are there any options, work-arounds ? )
I've read through this thread: https://github.com/facebook/react/issues/8598 but didn't find anything.
I want re-renders whenever an element is pushed to receivedElements.
Note that you won't get a re-render if you use:
this.state.receivedElements.push(newElement); // WRONG
That violates the restriction that you must not directly modify state. You'd need:
this.setState(function(state) {
return {receivedElements: state.receivedElements.concat([newElement])};
});
(It needs to be the callback version because it relies on the current state to set the new state.)
My question is, can I not trigger rendering when the array becomes empty ?
Yes — by not calling setState in that case.
It sounds as though receivedElements shouldn't be part of your state, but instead information you manage separately and reflect in state as appropriate. For instance, you might have receivedElements on the component itself, and displayedElements on state. Then:
this.receivedElements.push(newElement);
this.setState({displayedElements: this.receivedElements.slice()});
...and
// (...some operation that removes from `receivedElements`...), then:
if (this.receivedElements.length) {
this.setState({displayedElements: this.receivedElements.slice()});
}
Note how we don't call setState if this.receivedElements is empty.
What about useRef?
Documentation says:
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
So if you change ref value inside useEffect it won’t rerender component.
const someValue = useRef(0)
useEffect(() => {
someValue.current++
},[])

Categories

Resources