What's the best way to update a nested property deep inside the State object?
// constructor --
this.state.someprop = [{quadrangle: {rectangle: {width: * }}, ...}]
...
I want to update width of the rectangle object.
this.state.quadrangle.rectangle.width = newvalue // isn't working
I could make it work like:
const {quadrangle} = this.state
quadrangle.rectangle.width = newvalue
this.setState = {
quadrangle: quadrangle
}
But this method doesn't sound the best way for performance/memory
// ES6 WAYS TO UPDATE STATE
// NOTE: you MUST use this.setState() function for updates to your state
class Example extends Component {
constructor(props) {
super(props);
this.state = {
name: 'John',
details: {
age: 28,
height: 1.79,
}
}
componentDidMount() {
this.handleChangeName('Snow');
this.handleAgeChange(30);
}
componentDidUpdate() {
console.log(this.state);
/*
returns
{
name: 'Snow',
details: {
age: 30,
height: 1.79,
}
}
*/
}
// this way you keep your previous state immutable (best practice) with
// param "prevState"
handleChangeName = (_name) => {
this.setState(
(prevState) => ({
name: _name
})
)
}
//this is how you update just one property from an internal object
handleAgeChange = (_age) => {
this.setState(
(prevState) => ({
details: Object.assign({}, prevState.details, {
age: _age
})
})
)
}
// this is the simplest way to set state
handleSimpleAgeChange = (_age) => {
this.setState({
details: Object.assign({}, this.state.details, { age: _age })
})
}
render() {
return (
<h1>My name is {this.state.name} and I'm {this.state.details.age} years old</h1>
)
}
}
If you want to keep the best practice without making it harder, you can do:
updateState = (obj) => {
if (obj instance of Object) {
this.setState(
(prevState) => (Object.assign({}, prevState, obj))
);
}
}
usage:
//code ... code ... code ...
handleAgeChange = (_age) => {
this.updateState({
details: Object.assign({}, this.state.details, { age: _age }
})
}
The best way, and the way facebook has proposed, is to use this.setState({someProp: "your new prop"}).
Using it is the only way which is going to guarantee that the component will be rendered correctly.
This function is incremental, so you dont need to set the whole state, just the prop you need.
I strongly recomend you to read the docs here.
If your object is nested make the inner object it's own state,
this.state = {
quadrangle: {this.state.rectangle, ...}
rectangle: {width: * }}
};
then use your clone and replace technique:
const {rectangleNew} = this.state.rectangle;
rectangleNew.width = newvalue;
this.setState({rectangle: rectangleNew});
The state should propagate upwards. Should improve performance if only certain quadrangles need to be updated based on said rectangle. Props down, state up.
with hooks use this way
- setBorder((pre) => { return ({ ...pre, border_3: 2 }) })
example :
// state for image selected [ borderd ]
const [bordered, setBorder] = useState({ border_1: 0, border_2: 0, border_3: 0, border_4: 0, border_5: 0, border_6: 0, border_7: 0, border_8: 0 });
// pre is previous state value
const handle_chose = (y) => {
//generate Dynamic Key
var key = "border_" + y;
setBorder((pre) => { return ({ ...pre, [key]: 2 }) })
}
Related
I'm trying to modify an element in the array in a state. lets say I want to modify "id".
class example extends Component {
state = {
recipes: [{
id: 5
}]
}
changeID = (newID, arrayIndex) => {
}
What would I need to put in changeID to modify id?
You can use something like this:
function changeID(ID){
const { recipes } = this.state;
const filteredRecipes = recipes.filter(item => item.id === ID);
/*
Any logic to change the element here!
You can check if the element exists before doing anything.
*/
const recipe = filteredRecipes.shift();
const updatedRecipe = { ...recipe, id:6 }
//And if you want to update the state...
const updatedRecipes = [ ...recipes, updatedRecipe ];
this.setState({ recipes: updatedRecipes });
}
You can use setState combined with map (since your state is an array) to modify a given element with a specific id
changeID = (id, index) =>{
this.setState(prev =>{
return {
recipes : prev.recipes.map((recipe, i) =>{
if(index === i) return {...recipe, id}
return recipe
})
}
})
}
Change your code:
class Example extends Component{
constructor(props) {
super(props);
this.state = { recipes: [{ id: 5 }] };
}
changeID = (newID, arrayIndex) => {
let { recipes } = this.state;
recipes[arrayIndex].id = newID;
this.setState({ recipes: recipes }, () => console.log(this.state.recipes));
};
}
Here is the live working solution: Visit to see
Happy coding :)
In React, state should be readonly. To modify a value within an array, you should create a new array with just the one element updated.
class example extends Component{
state={
recipes: [
{id: 5}
]
}
changeID = (newID,arrayIndex)=>{
// create a shallow copy of the recipe with an updated id
const newRecipe = { ...recipes[arrayIndex], id: newID };
const newRecipes = recipes
.slice() // shallow copy the array
.splice(arrayIndex, 1, newRecipe); // replace the element at arrayIndex with new Recipe
this.setState({ recipes: newRecipes });
}
}
You can change array values by using the assignment operator = and index lookup []. E.g.
let arr = [3,4,5];
arr[0] = 0;
arr; // 0,4,5
For your example, that's:
let state = {
recipes: [{
id: 5
}]
};
let changeId = (newId, arrayIndex) =>
state.recipes[arrayIndex].id = newId;
changeId(4, 0);
console.log(state);
I am experienced js/React developer but came across case that I can't solve and I don't have idea how to fix it.
I have one context provider with many different state, but one state looks like following:
const defaultParams = {
ordering: 'price_asc',
page: 1,
perPage: 15,
attrs: {},
}
const InnerPageContext = createContext()
export const InnerPageContextProvider = ({ children }) => {
const [params, setParams] = useState({ ...defaultParams })
const clearParams = () => {
setParams({...defaultParams})
}
console.log(defaultParams)
return (
<InnerPageContext.Provider
value={{
params: params,
setParam: setParam,
clearParams:clearParams
}}
>
{children}
</InnerPageContext.Provider>
)
}
I have one button on page, which calls clearParams function and it should reset params to default value.
But it does not works
Even when i console.log(defaultParams) on every provider rerendering, it seems that defaultParams variable is also changing when state changes
I don't think it's normal because I have used {...defaultParams} and it should create new variable and then pass it to useState hook.
I have tried:
const [params, setParams] = useState(Object.assign({}, defaultParams))
const clearParams = () => {
setParams(Object.assign({}, defaultParams))
}
const [params, setParams] = useState(defaultParams)
const clearParams = () => {
setParams(defaultParams)
}
const [params, setParams] = useState(defaultParams)
const clearParams = () => {
setParams({
ordering: 'price_asc',
page: 1,
perPage: 15,
attrs: {},
})
}
None of above method works but 3-rd where I hard-coded same object as defaultParams.
The idea is to save dafult params somewhere and when user clears params restore to it.
Do you guys have some idea hot to make that?
Edit:
This is how I update my params:
const setParam = (key, value, type = null) => {
setParams(old => {
if (type) {
old[type][key] = value
} else old[key] = value
console.log('Params', old)
return { ...old }
})
}
please show how you update the "params".
if there is something like this in the code "params.attrs.test = true" then defaultParams will be changed
if old[type] is not a simple type, it stores a reference to the same object in defaultParams. defaultParams.attrs === params.attrs. Since during initialization you destructuring an object but not its nested objects.
the problem is here: old[type][key] = value
solution:
const setParam = (key, value, type = null) => {
setParams(old => {
if (type) {
old[type] = {
...old[type],
key: value,
}
} else old[key] = value
return { ...old }
})
}
I am facing the following issue and not able to figure it out.
I have two variables inside the state called userDetails & userDetailsCopy. In componentDidMount I am making an API call and saving the data in both userDetails & userDetailsCopy.
I am maintaining another copy called userDetailsCopy for comparison purposes.
I am updating only userDetails inside setState but even userDetailsCopy is also getting updated instead of have old API data.
Below is the code :
constructor(){
super()
this.state={
userDetails:{},
userDetailsCopy: {}
}
}
componentDidMount(){
// API will return the following data
apiUserDetails : [
{
'name':'Tom',
'age' : '28'
},
{
'name':'Jerry',
'age' : '20'
}
]
resp.data is nothing but apiUserDetails
/////
apiCall()
.then((reps) => {
this.setState({
userDetails: resp.data,
userDetailsCopy: resp.data
})
})
}
updateValue = (text,i) => {
let userDetail = this.state.userDetails
userDetail[i].name = text
this.setState({
userDetails: userDetail
})
}
submit = () => {
console.log(this.state.userDetials) // returns updated values
console.log(this.state.userDetailsCopy) // also return updated values instead of returning old API data
}
Need a quick solution on this.
The problem with this is that you think you are making a copy of the object in state by doing this
let userDetail = this.state.userDetails
userDetail.name = text
But, in Javascript, objects are not copied like this, they are passed by referrence. So userDetail at that point contains the referrence to the userDetails in your state, and when you mutate the userDetail it goes and mutates the one in the state.
ref: https://we-are.bookmyshow.com/understanding-deep-and-shallow-copy-in-javascript-13438bad941c
To properly clone the object from the state to your local variable, you need to instead do this:
let userDetail = {...this.state.userDetails}
OR
let userDetail = Object.assign({}, this.state.userDetails)
Always remember, Objects are passed by referrence not value.
EDIT: I didn't read the question properly, but the above answer is still valid. The reason userDetailCopy is being updated too is because resp.data is passed by referrence to both of them, and editing any one of them will edit the other.
React state and its data should be treated as immutable.
From the React documentation:
Never mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
Here are five ways how to treat state as immutable:
Approach #1: Object.assign and Array.concat
updateValue = (text, index) => {
const { userDetails } = this.state;
const userDetail = Object.assign({}, userDetails[index]);
userDetail.name = text;
const newUserDetails = []
.concat(userDetails.slice(0, index))
.concat(userDetail)
.concat(userDetails.slice(index + 1));
this.setState({
userDetails: newUserDetails
});
}
Approach #2: Object and Array Spread
updateValue = (text, index) => {
const { userDetails } = this.state;
const userDetail = { ...userDetails[index], name: text };
this.setState({
userDetails: [
...userDetails.slice(0, index),
userDetail,
...userDetails.slice(index + 1)
]
});
}
Approach #3: Immutability Helper
import update from 'immutability-helper';
updateValue = (text, index) => {
const userDetails = update(this.state.userDetails, {
[index]: {
$merge: {
name: text
}
}
});
this.setState({ userDetails });
};
Approach #4: Immutable.js
import { Map, List } from 'immutable';
updateValue = (text, index) => {
const userDetails = this.state.userDetails.setIn([index, 'name'], text);
this.setState({ userDetails });
};
Approach #5: Immer
import produce from "immer";
updateValue = (text, index) => {
this.setState(
produce(draft => {
draft.userDetails[index].name = text;
})
);
};
Note:
Option #1 and #2 only do a shallow clone. So if your object contains nested objects, those nested objects will be copied by reference instead of by value. So if you change the nested object, you’ll mutate the original object.
To maintain the userDetailsCopy unchanged you need to maintain the immutability of state (and state.userDetails of course).
function getUserDerails() {
return new Promise(resolve => setTimeout(
() => resolve([
{ id: 1, name: 'Tom', age : 40 },
{ id: 2, name: 'Jerry', age : 35 }
]),
300
));
}
class App extends React.Component {
state = {
userDetails: [],
userDetailsCopy: []
};
componentDidMount() {
getUserDerails().then(users => this.setState({
userDetails: users,
userDetailsCopy: users
}));
}
createChangeHandler = userDetailId => ({ target: { value } }) => {
const { userDetails } = this.state;
const index = userDetails.findIndex(({ id }) => id === userDetailId);
const userDetail = { ...userDetails[index], name: value };
this.setState({
userDetails: [
...userDetails.slice(0, index),
userDetail,
...userDetails.slice(index + 1)
]
});
};
render() {
const { userDetails, userDetailsCopy } = this.state;
return (
<React.Fragment>
{userDetails.map(userDetail => (
<input
key={userDetail.id}
onChange={this.createChangeHandler(userDetail.id)}
value={userDetail.name}
/>
))}
<pre>userDetails: {JSON.stringify(userDetails)}</pre>
<pre>userDetailsCopy: {JSON.stringify(userDetailsCopy)}</pre>
</React.Fragment>
);
}
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
I am looking for the best way to make my handleSwitch + setState function immutable. The code I have currently works but is mutable and I am unsure how to make it immutable everything I have tried either doesn't work or gives me a syntax error.
This is my state being initialised
this.state = {
contacts: [],
filteredContacts: [],
selected: [],
contactsSelection: {},
loading: false,
};
I have a simple <Switch /> comp which triggers the below when a user toggles the switch.
handleSwitch = async (contact, added) => {
if (added) {
console.log('added', { contact, added });
return this.setState(prevState => ({
contactsSelection: {
...prevState.contactsSelection,
[contact.id]: contact,
},
}));
}
console.log('removed', { contact, added });
// const { contactsSelection } = this.state;
return this.setState(prevState => {
const existingSelection = { ...prevState.contactsSelection };
delete existingSelection[contact.id]; // <-- make immutable
return { contactsSelection: existingSelection };
});
};
just to clarify: your code is fine by now, it does not mutate state directly but mutates copy.
but if you'd like having that in different way, here is a pattern:
this.setState(prevState => {
const { [contact.id]: unusedVariable, ...restContacts } =
prevState.contactsSelection;
return { contactsSelection: restContacts };
})
or with destructuring:
this.setState(
({ contactsSelection: {[contact.id]: unused, ...filteredContact} })) =>
({ contactsSelection: filteredContacts })
);
Anyway unfortunately there is no way to avoid introducing unusedVariable while destructuring. So it will need you to suppress ESLint warning "variable ... is not used".
To me, your way handling that is better readable.
I have a set of results from an api, stored in the state as the array 'results'.
I want a second array, 'visible_results' in the state. This should be a subset of 'results'. I'm trying to do this like this:
export default class SearchScreen extends Component {
constructor(props) {
super(props);
this.state = {
results: null,
visible_results: null,
low_price: null,
high_price: null,
min_price: null,
max_price: null
};
}
componentDidMount() {
const apiUrl = 'foo';
fetch(apiUrl)
.then(response => response.json())
.then(response => {
this.setState({
results: response.results,
min_price: 1,
max_price: 100
});
this.setState({
low_price: this.state.min_price,
high_price: this.state.max_price
});
});
}
handleChange = event => {
const { name, value } = event.target;
this.setState({
[name]: +value
});
this.setState({
visible_results: this.state.results.reduce((items, x) => {
if (this.state.low_price <= x.price && x.price <= this.state.high_price)
items.push(x);
return items;
})
});
};
The handleChange is tied to two sliders, one setting low_price, and one setting high_price. The function should then generate a subset of results, based on the new value of low_price or high_price, and save it to the state as visible_results.
It doesn't work. There are no errors, but visible_results always remain 'null'. The sliders definitely work. I've tried replacing the if statement with if (1==1) to make sure that it wasn't just an if statement typo. It did the same thing.
A few things:
Setting the values to null makes your code more complicated, I would either add a useful default value, e.g. min_price: 0, max_price: Infinity or just don't initialize it.
this.setState is asynchronous! If you call setState twice, the first call will be deferred, so this.state isn't yet update in the second call, so e.g. this.state.low_price inside the second call in handleChange wasn't yet updated.
If you don't pass an initial value to reduce, it will take the arrays first element, which in your case is an object, calling push on this won't work. You probably want .reduce(fn, []), but in your case .filter is actually more appropriate.
visible_results doesn't have to be part of the state as it is determined by other state props, so just determine it on render
Code:
export default class SearchScreen extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
low_price: 0,
high_price: Infinity,
min_price: 0,
max_price: Infinity
};
}
componentDidMount() {
const apiUrl = 'foo';
fetch(apiUrl)
.then(response => response.json())
.then(response => {
this.setState({
results: response.results,
min_price: 1,
max_price: 100,
min_price: 1,
high_price: 100,
});
});
}
handleChange = event => {
const { name, value } = event.target;
this.setState({
[name]: +value
});
}
render() {
const { results, low_price, high_price } = this.state;
const visibleResults = results.filter(item =>
low_price <= item.price && item.price <= high_price);
//...
}
}