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>
Related
I have a config file which contains an array that gets data from redux
"datapath": [
"common.registration.rgsnInfoRs[0].value", // street name
"common.registration.rgsnInfoRs[1].value", // city
"common.registration.rgsnInfoRs[2].value", // state
"common.registration.rgsnInfoRs[3].value" // zip code
]
In my react component, when I tried to render the data from this array it only returns back the data for the zip code. How can I make my component render all of index data in my render method?
constructor(props) {
super(props);
this.state = {
// data: '',
data: []
}
}
componentDidMount(){
if(_.get(this.props, 'datapath')){
_.map(this.props.datapath, (item, i) => {
let data=_.get(this.props.state,`i`);
this.setState({data})
})
}
}
// static getDerivedStateFromProps(props, state) {
// if(_.get(props, 'datapath')){
// _.map(props.datapath, (item, i) => {
// let data=_.get(props.state,item);
// state.data = data;
// })
// }
// }
static getDerivedStateFromProps(props, state) {
if(_.get(props, 'datapath')){
_.map(props.datapath, (item, i) => {
let data=_.get(props.state,item);
this.setState((prevState) => {
return { data: [...prevState.data, data] }
});
})
}
}
render() {
const {type, label} = this.props;
return (
type === "grid" ? (
<Grid.Column style={style}>
<strong>{label ? `${label}:` : null}</strong> {this.state.data}
</Grid.Column>
) : (
null
)
)
}
Your initial data type for this.state.data is an array. But in the following line, it gets the value of the item (in this case, it's either street name, city, state or zipcode). These values are string type and override the this.state.data datatype from array to string.
let data=_.get(props.state, `i`); // This is incorrect
You should append the value in the state data instead of assign in componentDidMount
componentDidMount() {
if(_.get(this.props, 'datapath')){
_.map(this.props.datapath, (item, i) => {
let data=_.get(this.props.state,`i`);
this.setState((prevState) => {
return { data: [...prevState.data, data] }
});
})
}
}
Now Let's look at getDerivedStateFromProps function. This is a static function and you can not use this inside a static function. According to the react docs, this function returns the updated state.
static getDerivedStateFromProps(props, state) {
const newData = [];
if(_.get(props, 'datapath')) {
_.map(props.datapath, (item, i) => {
let data=_.get(props.state,item);
newData.push(data);
});
return { data: newData };
}
}
I think that you are not asking the right questions. It is generally a bad idea to store data from props in state because it can lead to stale and outdated data. If you can already access it from props, then you don't need to also have it in state.
Your component receive a prop state which is an object and a prop datapath which is an array of paths used to access properties on that state. This is already weird and you should look into better patterns for accessing redux data through selector functions.
But if we can't change that, we can at least change this component. You can convert an array of paths to an array of values with one line of code:
const data = this.props.datapath.map( path => _.get(this.props.state, path) );
All of the state and lifecycle in your component is unnecessary. It could be reduced to this:
class MyComponent extends React.Component {
render() {
const { type, label = "", state, datapath = [] } = this.props;
const data = datapath.map((path) => _.get(state, path, ''));
return type === "grid" ? (
<Grid.Column style={style}>
<strong>{label}</strong> {data.join(", ")}
</Grid.Column>
) : null;
}
}
I think, when you are looping through the data and setting the state you are overriding the old data present in the state already.
may be you will have to include the previous data as well as set new data in it something like this.
this.setState({data : [...this.state.data, data]})
Here I implemented redux like store using custom hooks. everything goes well and code executed correctly but problem is that in reducer under switch statement "TOGGLE" there I return a updated state which is finally stored in globalstate but if I returned empty object {} instead of {products: updated} still globalstate updating correctly with a change that has been done in reducer...since i am not passing globalstate reference then how it is updated correctly
and what listeners exactly do in dispatch method in code
import MarkFavMulti from "./MarkFavMulti";
import classes from "./MarkFav.module.css";
import useStore from "../HookStore/Store";
import {reducer2} from "../SampleReducer";
const MarkFav = props => {
const [outfit, dispatch] = useStore(reducer2);
const onClicked = (id) => {
dispatch({type: "TOGGLE", id: id});
}
const element = outfit.products.map((item) => {
return <MarkFavMulti cloth={item.name}
favorite={item.favorite}
price={item.price}
key={item.id}
clicked={onClicked.bind(this, item.id)} />
});
return (
<main className={classes.Markfav}>
{element}
</main>
);
};
export default MarkFav;
import {useState, useEffect} from "react";
let globalState = {};
let listeners = [];
const useStore = (reducer) => {
const setState = useState(globalState)[1];
const dispatch = (action) => {
let curr = Object.assign({},globalState);
const newState = reducer({...curr}, action)
globalState = {...globalState,...newState};
for(let listener of listeners) {
listener(globalState);
}
};
useEffect(()=>{
listeners.push(setState);
return () => {
listeners.filter(item => item !==setState);
}
},[setState]);
return [globalState, dispatch];
};
export const initStore = (initialState) => {
if(initialState) {
globalState = {...globalState, ...initialState};
}
}
export default useStore;
let initialState = {
products: [
{ id: 1, name: "shirt", price: "$12", favorite: false },
{ id: 2, name: "jeans", price: "$42", favorite: false },
{ id: 3, name: "coat", price: "$55", favorite: false },
{ id: 4, name: "shoes", price: "$8", favorite: false },
]
}
const configureStore = () => {
initStore(initialState);
};
export default configureStore;
export const reducer2 = (state=initialState, action) => {
switch (action.type) {
case "TOGGLE":
let update = {...state};
let updated = [...update.products];
updated = updated.map(item => {
if(item.id === action.id) {
item.favorite = !item.favorite;
return item;
}
return item;
});
return {products: updated};
//if we return {} ...it will updated correctly in globalstate
default:
throw new Error("not reachable");
}
}
The behavior that you are describing is due to this object assignment right here:
item.favorite = !item.favorite;
Here you are directly mutating the properties of the item object. You probably thought that it would be fine since you are using a copy of the products array.
let update = {...state};
let updated = [...update.products];
What actually happens is that updated is a "shallow copy" of the original array. The array itself is a new array, but the items in that array are the same items as in the state. You can read more about that here.
You need to return a new item object instead of mutating it. Here's a concise way to write it using the ternary operator.
case "TOGGLE":
return {
...state, // not actually necessary since products is the only property
products: state.products.map((item) =>
item.id === action.id
? {
...item,
favorite: !item.favorite
}
: item
)
};
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 basically want to put each string in the array in a separated div, I'm trying to do this but is not rendering anything
export default class Loja extends Component {
state = {
loja: {},
lojaInfo: {},
category: []
}
async componentDidMount() {
const { id } = this.props.match.params;
const response = await api.get(`/stores/${id}`);
const { category, ...lojaInfo } = response.data
this.setState({ loja: category, lojaInfo });
console.log(category)
}
render() {
const { category } = this.state;
return (
<p>{category.map(cat => <div>{cat}</div>)}</p>
);
}
}
The console.log(category) shows this:
The error is that you're updating 2 properties inside the componentDidMount, one is loja: category and the second property is lojaInfo, but in the render() method you're accessing this.state.category which still is an empty string.
What you want to do instead is, inside of your componentDidMount update your state like this:
this.setState({
category,
lojaInfo,
});
you've added your category into the loja object in the state.
something like this should work:
async componentDidMount() {
const { id } = this.props.match.params;
const response = await api.get(`/stores/${id}`);
const { category, ...lojaInfo } = response.data
this.setState({ category, lojaInfo });
}
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 }) })
}