I don't know exactly what it is, but I have run into countless problems in trying to do the simplest state updates on arrays using hooks.
The only thing that I have found to work is using the useReducer to perform a single update on the array with putting dispatch on onClick handlers. In my current project, I am trying to update array state in a for loop nested in a function that runs on a form submit. I have tried many different solutions, and this is just one of my attempts.
function sessionToState(session) {
let formattedArray = []
for (let i = 0; i < session.length; i++) {
formattedArray.push({ url: session[i] })
setLinksArray([...linksArray, formattedArray[i]])
}
}
// --------------------------------------------------------
return (
<div>
<form
method="post"
onSubmit={async e => {
e.preventDefault()
const session = await getURLs({ populate: true })
sessionToState(session)
await createGroup()
I was wondering if there are any big things that I am missing, or maybe some great tips and tricks on how to work with arrays using hooks. If any more information is needed don't hesitate to ask. Thanks.
I was wondering if there are any big things that I am missing
TLDR: setLinksArray does not update linksArray in the current render, but in the next render.
Assuming the variables are initialized as follows:
const [linksArray, setLinksArray] = useState([])
A hint is in the const keyword, linksArray is a constant within 1 render (and this fact wouldn't change with let, because it's just how useState works).
The idea of setLinksArray() is to make a different constant value in the next render.
So the for loop would be similar to:
setLinksArray([...[], session0])
setLinksArray([...[], session1])
setLinksArray([...[], session2])
and you would get linksArray = [session2] in the next render.
Best way to keep sane would be to call any setState function only once per state per render (you can have multiple states though), smallest change to your code:
function sessionToState(session) {
let formattedArray = []
for (let i = 0; i < session.length; i++) {
formattedArray.push({ url: session[i] })
}
setLinksArray(formattedArray)
}
Furthermore, if you need to perform a side effect (like an API call) after all setState functions do their jobs, i.e. after the NEXT render, you would need useEffect:
useEffect(() => {
...do something with updated linksArray...
}, [linksArray])
For a deep dive, see https://overreacted.io/react-as-a-ui-runtime
When invoking state setter from nested function calls you should use functional update form of setState. In your case it would be:
setLinksArray(linksArray => [...linksArray, formattedArray[i]])
It is not exactly clear what kind of problems you encounter, but the fix above will save you from unexpected state of linksArray.
Also this applies to any state, not only arrays.
Performance wise you shouldn't call setState every iteration. You should set state with final array.
const sessionToState = (session) => {
setLinksArray(
session.map(sessionItem => ({url: sessionItem}))
);
}
... or if you want to keep old items too you should do it with function inside setState ...
const sessionToState = (session) => {
setLinksArray(oldState => [
...oldState,
...session.map(sessionItem => ({url: sessionItem}))
]);
}
Related
Probably it is a classic issue with useState which is not updating.
So there is a tree with some checkboxes, some of them are already checked as they map some data from an endpoint.
The user has the possibility to check/uncheck them. There is a "cancel" button that should reset them to the original form.
Here is the code:
const [originalValues, setOriginalValues] = useState<string[]>([]);
...
const handleCancel = () => {
const originalValues = myData || []; //myData is the original data stored in a const
setOriginalValues(() => [...myData]);
};
...
useEffect(() => {
setOriginalValues(originalValues);
}, [originalValues]);
However, it is not working, the tree is not updating as it should. Is it something wrong here?
Just do the following, no need for ()=> the state will update inside the hook if called, plus change the constant it will cause confusion inside your code and protentional name clash later on, with the current state variable name, and also make sure your data are there and you are not injection empty array !!!! which could be the case as well !.
// Make sure data are available
console.log(myData)
// Then change the state
setOriginalValues([...myData]);
Is there a reason that calling setSate() in a loop would prevent it from updating the state multiple times?
I have a very basic jsbin that highlights the problem I am seeing. There are two buttons. One updates the state's counter by 1. The other calls the underlying function of One in a loop -- which seemingly would update the state multiple times.
I know of several solutions to this problem but I want to make sure that I am understanding the underlying mechanism here first. Why can't setState be called in a loop? Do I have it coded awkwardly that is preventing the desired effect?
From the React Docs:
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
Basically, don't call setState in a loop. What's happening here is exactly what the docs are referring to: this.state is returning the previous value, as the pending state update has not been applied yet.
There's a nice way to update state in a loop. Just make an empty variable, set its value to the updated state, call setState(), and pass it this variable:
const updatedState = {};
if (vars.length) {
vars.forEach(v => {
updatedState[v] = '';
this.setState({
...this.state
...updatedState,
});
});
}
You have to use something like that:
const MyComponent = () => {
const [myState, setMyState] = useState([]);
const handleSomething = (values) => {
values.map((value) => {
setMyState((oldValue) => [...oldValue, { key: value.dataWhatYouWant }]);
}
}
return (<> Content... </>);
}
I had the same problem. But tried with a little different approach.
iterateData(data){
//data to render
let copy=[];
for(let i=0;<data.length;i++){
copy.push(<SomeComp data=[i] />)
}
this.setState({
setComp:copy
});
}
render(){
return(
<div>
{this.state.setComp}
</div>
);
}
I hope this helps.
Basically setState is called asynchronously. It also has a callback function which you can utilise to do something once the state has been mutated.
Also if multiple setStates are called one after the other they are batched together as written previously.
Actually setState() method is asynchronous. Instead you can achieve it like this
manyClicks() {
var i = 0;
for (i = 0; i < 100; i++) {
//this.setState({clicks: this.state.clicks + 1}); instead of this
this.setState((prevState,props)=>({
clicks: ++prevState.clicks
}))
}
}
I was having this issue when creating a feature to import items.
Since the amount of the importing items could be huge, I need to provide feedback (like a progress bar) to the site user so that they know that they aren't sitting there and waiting for nothing.
As we know that we can't setState in a loop, I took a different approach by running the task recursively.
Here's a example code
https://codesandbox.io/s/react-playground-forked-5rssb
You can try this one using the previous value to increase the count.
function handleChange() {
for (let i = 0; i < 5; i++) {
setState(prev => {
return prev + 1
})
}
}
I was able to make your code work, calling setState in the loop by doing the following:
manyClicks() {
for (i = 0; i < 100; i++) {
this.setState({clicks: this.state.clicks += 1})
}
}
enter code here
Hopefully this helps!
In react I am using functional component and I have two functions (getBooks) and (loadMore)
getBooks get data from an endPoint. But when I call loadMore function on button click inside the getBooks function (loadMoreClicked) is not changed it uses the previous state even after calling it with a delay of (5 seconds). But when I call loadMore again the state changes and everything works fine.
can someone explain why the (loadMoreClicked) on the initial call to (getBooks) didn't update
even calling it after 5 seconds delay.
function component() {
const [loadMoreClicked, setLoadMore] = useState(false);
const getBooks = () => {
const endPoint = `http://localhost/getBooks`; //this is my end point
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
console.log(loadMoreClicked); //the (loadMoreClicked) value is still (false) after (5 sec)
})
.catch(err => {
console.log(err);
});
};
const loadMore = () => {
setLoadMore(true); //here i am changing (loadMoreClicked) value to (true)
setTimeout(() => {
getBooks(); // i am calling (getBooks()) after 5 seconds.
}, 5000);
};
return (
<div>
<button onClick={() => loadMore()}>loadMore</button> //calling (loadMore)
function
</div>
);
}
There's two things going on:
getBooks() is using const values that are defined in the surrounding function. When a function references const or let variables outside of its definition, it creates what's called a closure. Closures take the values from those outer variables, and gives the inner function copies of the values as they were when the function was built. In this case, the function was built right after the state was initially called, with loadMoreClicked set to false.
So why didn't setLoadMore(true) trigger a rerender and rewrite the function? When we set state, a rerender doesn't happen instantaneously. It is added to a queue that React manages. This means that, when loadMore() is executed, setLoadMore(true) says "update the state after I'm done running the rest of the code." The rerender happens after the end of the function, so the copy of getBooks() used is the one built and queued in this cycle, with the original values built in.
For what you're doing, you may want to have different functions called in your timeout, depending on whether or not the button was clicked. Or you can create another, more immediate closure, based on whether you want getBooks() to consider the button clicked or not, like so:
const getBooks = wasClicked => // Now calling getBooks(boolean) returns the following function, with wasClicked frozen
() => {
const endPoint = `http://localhost/getBooks`;
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
console.log(wasClicked); // This references the value copied when the inner function was created by calling getBooks()
})
.catch(err => {
console.log(err);
});
}
...
const loadMore = () => {
setLoadMore(true);
setTimeout(
getBooks(true), // Calling getBooks(true) returns the inner function, with wasClicked frozen to true for this instance of the function
5000
);
};
There is a third option, which is rewriting const [loadMoreClicked, setLoadMore] to var [loadMoreClicked, setLoadMore]. While referencing const variables freezes the value in that moment, var does not. var allows a function to reference the variable dynamically, so that the value is determined when the function executes, not when the function was defined.
This sounds like a quick and easy fix, but it can cause confusion when used in a closure such as the second solution above. In that situation, the value is fixed again, because of how closures work. So your code would have values frozen in closures but not in regular functions, which could cause more confusion down the road.
My personal recommendation is to keep the const definitions. var is being used less frequently by the development community because of the confusion of how it works in closures versus standard functions. Most if not all hooks populate consts in practice. Having this as a lone var reference will confuse future developers, who will likely think it's a mistake and change it to fit the pattern, breaking your code.
If you do want to dynamically reference the state of loadMoreClicked, and you don't necessarily need the component to rerender, I'd actually recommend using useRef() instead of useState().
useRef creats an object with a single property, current, which holds whatever value you put in it. When you change current, you are updating a value on a mutable object. So even though the reference to the object is frozen in time, it refers to an object that is available with the most current value.
This would look like:
function component() {
const loadMoreClicked = useRef(false);
const getBooks = () => {
const endPoint = `http://localhost/getBooks`;
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
console.log(loadMoreClicked.current); // This references the property as it is currently defined
})
.catch(err => {
console.log(err);
});
}
const loadMore = () => {
loadMoreClicked.current = true; // property is uodated immediately
setTimeout(getBooks(), 5000);
};
}
This works because, while loadMoreClicked is defined as a const at the top, it is a constant reference to an object, not a constant value. The object being referenced can be mutated however you like.
This is one of the more confusing things in Javascript, and it's usually glossed over in tutorials, so unless you're coming in with some back-end experience with pointers such as in C or C++, it will be weird.
So, for what you are doing, I'd recommend using useRef() instead of useState(). If you really do want to rerender the component, say, if you want to disable a button while loading the content, then reenable it when the content is loaded, I'd probably use both, and rename them to be clearer as to their purpose:
function component() {
const isLoadPending = useRef(false);
const [isLoadButtonDisabled, setLoadButtonDisabled] = useState(false);
const getBooks = () => {
const endPoint = `http://localhost/getBooks`;
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
if (isLoadPending.current) {
isLoadPending.current = false:
setLoadButtonDisabled(false);
}
})
.catch(err => {
console.log(err);
});
};
const loadMore = () => {
isLoadPending.current = true;
setLoadButtonDisabled(true);
setTimeout(getBooks(), 5000);
};
}
It's a little more verbose, but it works, and it separates your concerns. The ref is your flag to tell your component what it's doing right now. The state is indicating how the component should render to reflect the button.
Setting state is a fire-and-forget operation. You won't actually see a change in it until your component's entire function has executed. Keep in mind that you get your value before you can use the setter function. So when you set state, you aren't changing anything in this cycle, you're telling React to run another cycle. It's smart enough not to render anything before that second cycle completes, so it's fast, but it still runs two complete cycles, top to bottom.
you can use the useEffect method to watch for loadMoreClicked updates like componentDidUpdate lifecycle method and call the setTimeout inside that,
useEffect(() => {
if(loadMoreClicked){
setTimeout(() => {
getBooks();
}, 5000);
}
}, [loadMoreClicked])
this way only after the loadMoreClicked is changed to true we are calling the setTimeout.
This boils down to how closures work in JavaScript. The function given to setTimeout will get the loadMoreClicked variable from the initial render, since loadMoreClicked is not mutated.
I am using React Hooks to manage states within a component.
const addNode = () => {
let pform = pForm
let handles = [vForm, yForm, hForm]
let access_info = [virtualForm, management1Form, management2Form, consoleForm]
let newObj = {
...currentForm,
p: pform,
handles: handles,
access_info: access_info,
}
console.log('newObj', newObj)
setCurrentForm(
newRouterObj
)
console.log(currentForm)
let currArr = [...addedNodes]
currArr.push(currentForm)
setAddedNodes(currArr)
intializeForms()
}
The function above is an onClick that I use when I press an Add button. The forms (pForm, vForm, yForm, etc.) are all separate states. I gather them together and put them into a single object newObj and use setCurrentForm to update the currentForm state to newObj.
When I console.log the newObj, everything goes in fine. However, when I check the currentForm after the setCurrentForm, the fields (p, handles, and access_info) are empty.
I know that states in React can have a delay in updates so I might have to use useEffect. However, in my use case, which is to gather different states and put them in as a new field in the currentForm state seems useEffect is not the best way to solve it. Can anyone help please?
You are misunderstanding exactly how useState works. When you call the useState setter function, the state value isn't actually updated immediately, instead it will trigger the component to re-render with the updated value. Even though you call the setter half way through the function, the state value will remain the original value for the entire lifetime of that function call.
You could slightly tweak what you have to be
const addNode = () => {
...
let currArr = [...addedNodes]
// you know that currentForm is supposed to be newObj, so just push that
// see my explanation above to understand why it currentForm isn't what you expect
currArr.push(newObj)
...
}
It's an async action so values will not be assigned/updated instantly. You need to watch for the changes using useEffect hook to log new values and to do anything in case
useEffect(() => {
// Whenever `currentForm` will be updated, this callback will be invoked
console.log('updated currentForm values', currentForm);
},[currentForm]);
I'm pulling data into one of my parent components and then using various filter statements which are based on user choices from select boxes. I'm then calling an action which simply stores that filtered data based on the users search into global state so that my child components can access them.
One of my child components is supposed to render the results but what is happening is the results being rendered are lagging one action behind. I've encountered similar issues when using set state and my solution then was to use a callback but I'm not exactly sure how to go about dealing with this issue in this situation with redux.
The wordpress.get is just named import of axios config.
componentDidMount = async () => {
const response = await wordpress.get(
"*********************/api/wp/v2/variants?per_page=100"
);
this.props.fetchData(response);
const data = []
response.data.forEach(ele => {
data.push(ele)
})
this.props.sendFilteredView(data);
};
handleChange = () => {
this.preBuiltFiltering();
};
I've left out pre-built filtering because its long and excessive, all it does is run the filter based on the users choices and then dispatches the this.props.sendFilteredView action with the filtered data set as the argument. The action just returns the payload.
I then am rendering the results of the filter in a child component by accessing the global state (I also tried just passing it directly through props, same issue).
It’s an async function, you’re using a callback after the forEach with data.
So you need to wait forEach been completed.
Try to use await before forEach.
componentDidMount = async () => {
const response = await wordpress.get(
"*********************/api/wp/v2/variants?per_page=100"
);
this.props.fetchData(response);
const data = []
await response.data.forEach(ele => {
data.push(ele)
})
this.props.sendFilteredView(data);
};
handleChange = () => {
this.preBuiltFiltering();
};