Is it the right way to call setState as a callback in another one? Here a piece of my code.
This is my initial state:
state = {
resumeCollection: [],
filters: {
educationLevels: [],
educationFields: [],
jobAdvertisementId: []
}
};
And this a componentDidMount section:
componentDidMount() {
this.setState(prevState =>( {filters : {
jobAdvertisementId : [...prevState.filters.jobAdvertisementId, {value: this.props.router.query.value, id: this.props.router.query.id}]}
}
)
, () => this.setState(state => {
return({
resumeCollection : state.resumeCollection.filter(resume => resume.jobAdvertisementId == state.filters.jobAdvertisementId[0].id )
});
}
)
)
}
I would advice against doing 2 state updates like that as it will introduce a useless second render.
If you need to set portion of the state based on another portion of the state, you can calculate it and store it outside the return statement inside a variable and used it where you need it.
In your case it might look something like this:
componentDidMount() {
this.setState(prevState => {
const nextJobAdvertisementId = [
...prevState.filters.jobAdvertisementId,
{
value: this.props.router.query.value,
id: this.props.router.query.id
}
];
return {
filters: {
jobAdvertisementId: nextJobAdvertisementId
},
resumeCollection: prevState.resumeCollection.filter(
resume => resume.jobAdvertisementId === nextJobAdvertisementId[0].id
)
};
});
}
Yes you can add another setState as a part of callback to the first setState(), when you want to set another state based on first.
In the below example I set 'b' state based on state 'a':
e.g
const setB = () => {
if(this.state.a)
this.setState({b:"Success"})
else
this.setState({b:"failure"})
}
this.setState({a:true},this.setB)
Related
I am using React and have the following object:
const [records, setRecords] = useState(
[
{id: 1, correct: false},
{id: 2, correct: false},
{id: 3, correct: false},
{id: 4, correct: false},
{id: 5, correct: false}
]);
To update the object I have the following:
const onCorrectAnswerHandler = (id, correct) => {
setRecords(
records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
);
}
Here's the problem:
I want to run another function called isComplete after it but within the Handler function, that uses the changed records object, but it appears to use the original unchanged 'records' object (which console.log confirms).
e.g.
const onCorrectAnswerHandler = (id, correct) => {
setRecords(
records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
);
isComplete(records);
}
Console.log(records) confirms this. Why does it not use the updated records since the isComplete function runs after the update, and how can I get it to do this?
Try renaming the function as React sees no change in the object and likewise when you are using an array or object in a state. Try to copy them out by storing them in a new variable.
setRecords(
const newRecords = records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
//seting this now triggers an update
setRecords(newRecords);
);
Then as per react documentation it's better to listen to changes with lifecycle methods and not setting state immediately after they are changed because useState is asynchronous.
so use useEffect to listen to the changes to set is Complete
useEffect(() => {
isComplete(records)
}, [records])
I hope this helps you?
This is due to the fact that setState is not actually synchronous. The stateful value is not updated immediately when setState is called, but on the next render cycle. This is because React does some behind the scenes stuff to optimise re-renders.
There are multiple approaches to get around this, one such approach is this:
If you need to listen to state updates to run some logic you can use the useEffect hook.
useEffect(() => {
isComplete(records)
}, [records])
This hook is pretty straightforward. The first argument is a function. This function will run each time if one of the variables in the dependency array updates. In this case it will run each time records update.
You can modify above function onCorrectAnswerHandler to hold the updated records in temporary variable and use to update state and call isComplete func
const onCorrectAnswerHandler = (id, correct) => {
let _records = records.map((record) => {
if (record.id === id) {
return {
id: id,
correct: true
};
} else {
return record;
}
})
setRecords(_records);
isComplete(_records);
}
try this please.
const onCorrectAnswerHandler = (id) => {
records.forEach(r =>{
if(r.id == id) r.correct=true;
});
setRecords([...records]);
}
I'm extremely new with Reactjs and Redux, anyone can explain what is Mutable and Immutable in reactjs/redux?
I'm using Redux to update state, and using it to control my UI layout, but I found one of the method below will not working, although both are also updating the object.
I've a store object such as below:
const initialAppState = {
showSideBar: false,
showSwitcher: false,
showRightPane: false,
menu: [
{
name: "Home",
redirect: "/",
},
{
name: "About",
redirect: "/about",
expanded: false,
childs: [{
name: "Child one",
redirect: "/Child"
}]
},
]
}
Beside that, I have a Pane.js and Menu.js to render my menu list.
Pane.js
const LeftPane = (props) => {
return <List>
{links.map((o, index) => {
return <Menus key={o.name} props={o} index={index} />
})}
</List>
}
Menu.js
const Menus = ({ props, index, onToggleSubMenu }) => {
return <ListItem> ...
}
I'm trying to update the expanded value to true for the menu object, but when I using state.menu.map to change the value, my react component won't re-render? It will execute to the Pane.js, and I can see the expanded = true from the Pane.js props. but it won't execute to the Menu.js?
const AppReducer = (state = initialAppState, action) => {
return {...state,
menu: [
...state.menu.map((m, i) => {
if (i === action.index) {
m.expanded = !state.menu[action.index].expanded;
}
return m;
})
]
}
}
On the other hand, if I update the expanded value from the code below, it works.
const AppReducer = (state = initialAppState, action) => {
state.menu[action.index] = Object.assign({}, state.menu[action.index], {
expanded: !state.menu[action.index].expanded,
});
return {...state, menu: state.menu}
}
What is the different between these two? What is the correct way to update state? Why we should use Object.assign or spread (...) operator to update state? I've read the Redux Immutale Update Patterns, the working code above is more like the Common Mistake mentioned in the link.
There are two things which you should do differently in your AppReducer.
Map function returns a new array and does not mutate the original array, so no need to destruct there.
Inside your map function, you have the reference to the object m, and you are mutating the m by changing m.expanded to !m.expanded. This is where you should actually be returning a new object.
You should write AppReducer as following.
const AppReducer = (state = initialAppState, action) => {
return {
...state,
// No need to destruct when using map, map always returns a new array
menu: state.menu.map((m, i) => {
if (i === action.index) {
// Return a new object, with all properties copied, and expanded's value toggled
return {
...m,
expanded: !m.expanded;
}
}
// Return the original object because no change has been made
return m;
}),
};
};
As for the difference between the spread operator and Object.assign, according to object-rest-spread-proposal, one is syntactic sugar of the other, i.e. {...a} is pretty much an easier way to write Object.assign({}, a);
I am having a onChange function i was trying to update the array optionUpdates which is inside of sentdata by index wise as i had passed the index to the onChange function.
Suppose i update any two values of the input field from option which is inside of postdata therefore the input name i.e. orderStatus with changed value and with order should be saved inside of optionUpdates
For example: Suppose i update the option 1 and option 3 of my postdata further inside of options of orderStatus values so my optionUpdates which is inside of sentdata should look like this
optionUpdates: [
{
Order: 1,
orderStatus: "NEW1"
},
{
Order: 3,
orderStatus: "New2"
}
]
here is what i tried
setSentData(oldValue => {
const curoptions = oldValue.sentdata.optionUpdates[idx];
console.log(curoptions);
curoptions.event.target.name = event.target.value;
return {
...oldValue,
sentdata: {
...oldValue.sentdata.optionUpdates,
curoptions
}
};
});
};
Demo
complete code:
import React from "react";
import "./styles.css";
export default function App() {
const x = {
LEVEL: {
Type: "LINEN",
options: [
{
Order: 1,
orderStatus: "INFO",
orderValue: "5"
},
{
Order: 2,
orderStatus: "INPROGRESS",
orderValue: "5"
},
{
Order: 3,
orderStatus: "ACTIVE",
orderValue: "9"
}
],
details: "2020 N/w UA",
OrderType: "Axes"
},
State: "Inprogress"
};
const [postdata, setPostData] = React.useState(x);
const posting = {
optionUpdates: []
};
const [sentdata, setSentData] = React.useState(posting);
const handleOptionInputChange = (event, idx) => {
const target = event.target;
setPostData(prev => ({
...prev,
LEVEL: {
...prev.LEVEL,
options: prev.LEVEL.options.map((item, id) => {
if (id === idx) {
return { ...item, [target.name]: target.value };
}
return item;
})
}
}));
setSentData(oldValue => {
const curoptions = oldValue.sentdata.optionUpdates[idx];
console.log(curoptions);
curoptions.event.target.name = event.target.value;
return {
...oldValue,
sentdata: {
...oldValue.sentdata.optionUpdates,
curoptions
}
};
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
key={idx}
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e, idx)}
/>
);
})}
</div>
);
}
If I've understood correctly then what you're looking to do is save a copy of the relevant options object in sentdata every time one changes. I think the best way to approach this is by doing all your state modification outside of setPostData, which then makes the results immediately available to both setPostData and setSentData. It will also make the setters easier to read, which is good because you have some quite deeply nested and complicated state here.
A few other things worth noting first:
Trying to use synchronous event results directly inside the asynchronous setter functions will throw warnings. If you do need to use them inside setters, then it is best to destructure them from the event object first. This implementation uses destructuring although it didn't end up being necessary in the end.
You seem to have got a bit muddled up with setSentData. The oldValue parameter returns the whole state, as prev in setPostData does. For oldValue.sentdata you just wanted oldValue. You also wanted curoptions[event.target.name], not curoptions.event.target.name.
So, on to your code. I would suggest that you change the way that your input is rendered so that you are using a stable value rather than just the index. This makes it possible to reference the object no matter which array it is in. I have rewritten it using the Order property - if this value is not stable then you should assign it one. Ideally you would use a long uuid.
{postdata.LEVEL.options.map(item => {
return (
<input
key={item.Order}
type="text"
name="orderStatus"
value={item.orderStatus}
onChange={e => handleOptionInputChange(e, item.Order)}
/>
);
})}
The handleOptionInputChange function will now use this Order property to find the correct objects in both postdata and sentdata and update them, or if it does not exist in sentdata then push it there. You would do this by cloning, modifying, and returning the relevant array each time, as I explained before. Here is the function again with comments:
const handleOptionInputChange = (event, orderNum) => {
const { name, value } = event.target;
/* Clone the options array and all objects
inside so we can mutate them without
modifying the state object */
const optionsClone = postdata.LEVEL.options
.slice()
.map(obj => Object.assign({}, obj));
/* Find index of the changed object */
const optionIdx = optionsClone.findIndex(obj => obj.Order === orderNum);
/* If the orderNum points to an existing object...*/
if (optionIdx >= 0) {
/* Change the value of object in clone */
optionsClone[optionIdx][name] = value;
/* Set postdata with the modified optionsClone */
setPostData(prev => ({
...prev,
LEVEL: {
...prev.LEVEL,
options: optionsClone
}
}));
/* Clone the optionUpates array and all
contained objects from sentdata */
const updatesClone = sentdata.optionUpdates
.slice()
.map(obj => Object.assign({}, obj));
/* Find the index of the changed object */
const updateIdx = updatesClone.findIndex(obj => obj.Order === orderNum);
/* If the changed object has already been
changed before, alter it again, otherwise push
a new object onto the stack*/
if (updateIdx >= 0) {
updatesClone[updateIdx][name] = value;
} else {
updatesClone.push({ Order: orderNum, [name]: value });
}
/* Set sentdata with modified updatesClone */
setSentData(prev => ({
...prev,
optionUpdates: updatesClone
}));
}
};
I am creating a todo-list, the following function handleChange gets the id of the a todo component and changes its attribute of completed from true/false. This is then saved in state of allTodos
function handleChange(id) {
const updatedTodos = allTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
setTodos(updatedTodos)
}
const todoComponents = allTodos.map(item => <Todos key={item.id} item={item} handleChange={handleChange}/>)
the function updateDB takes that value from state and using it to update the database.
function updateDB(event) {
event.preventDefault()
const value = {
completed: false,
text: newTodo,
id: allTodos.length,
}
}
Here's where the problem arises: id: allTodos.length. If one of these are deleted, it will create a todo with a duplicate ID, crashing the whole thing. I don't know how to avoid this problem.
In updateDB, you are setting id to allTodos.length aka 1.
My Requirement is to update the state value in map function of componentWillReceiveProps.
In console log all I am getting is 1s but sub.subscribed contain 0s and 1s
Reference of console window: http://prntscr.com/jqifiz
constructor(props) {
super(props);
this.state = {
regionAll: [],
};
}
componentWillReceiveProps(nextProps){
if(nextProps.apiData !== false ){
nextProps.apiData.data.datacenter.category.map((sub)=> {
console.log(sub.subscribed,'sub.subscribed');
this.setState({
regionAll: [
...this.state.regionAll,
sub.subscribed
]
},()=>{
console.log(this.state.regionAll,'sub');
})
})
}
Is this a correct way to update state in reactjs?
setState is async.In Array#map, it called multiple time.Only last value is added in array regionAll and not all because of async setState call with multiple value.
You can collect all sub.subscribed value in single array with Array#reducer then perform state update.
if (nextProps.apiData !== false) {
let sub = nextProps
.apiData
.data
.datacenter
.category
.reduce((accum, sub) => [
...accum,
sub.subscribed
], [])
this.setState({
regionAll: [...sub]
}, () => {
console.log(this.state.regionAll, 'sub');
})
}
The problem arises because setState calls are batched and you are updated React state based on prevState, you should instead use functional state for such cases
componentWillReceiveProps(nextProps){
if(nextProps.apiData !== false ){
nextProps.apiData.data.datacenter.category.map((sub)=> {
console.log(sub.subscribed,'sub.subscribed');
this.setState(prevState => ({
regionAll: [
...prevState.regionAll,
sub.subscribed
]
}),()=>{
console.log(this.state.regionAll,'sub');
})
})
}
However its a bad idea to call setState in a map, you can instead get the data from map and call setState just once like
componentWillReceiveProps(nextProps){
if(nextProps.apiData !== false ){
const subscribed = nextProps.apiData.data.datacenter.category.map((sub)=> {
console.log(sub.subscribed,'sub.subscribed');
return sub.subscribed;
})
this.setState(prevState => ({
regionAll: [
...this.state.regionAll,
...subscribed
]
}),()=>{
console.log(this.state.regionAll,'sub');
})
}