Reputation: 256
I have used redux with react in multiple web applications. Currently I am developing my first React Native app with Redux.
I have encountered a very weird problem.
I have created a store and passed it to Provider component which renders the App component as a child. (Basic usage of react-redux)
In the app I have a connected component. It passes data received from the store to a Navigator Route while calling navigator.push(route). The component at this route is not a connected component. It receives the props and stores the props in its state. The props are not simply literals but they are objects/arrays. Depending on the user interaction, this component updates it's state through setState. This operation is directly updating the store.
Am I not supposed set the props on the component state which are received from the store through matchStateToProps? Though that was the case , the setState is happening in different component. The store should not simply update itself.
I am confused. Please help.
(If the problem is not clear or confusing I will add the relevant code snippets from my code)
EDIT 1 :
Here is a fiddle which conveys my problem
const intialState = {
0: {
orgId: 0,
peopleInfo: {
0 : {
pID: 0,
name: 'parent',
children: [
{
userID: 1,
name: 'abc',
},
{
userID: 2,
name: 'xyz',
},
]
}
}
}
}
function reducer (currentState, action) {
currentState = intialState;
console.log(currentState[0].peopleInfo[0]); // priniting the store every time an action is dispatched
// NO CHANGES TO THE STORE WHEN ACTION IS DISPATCHED
return currentState;
}
// Create Store
var store = Redux.createStore(reducer);
// action creator which will simply trigger the reducer
function save(){
return {
type: 'SAVE'
}
}
// Presentational Components (No state, only props and render)
var OrgsContainer = React.createClass({
render() {
return (
<div>
<div>
<div>1. Open console first</div>
<div>2. Change parent name - no change in the name property for the record on the store </div>
<div>3. Change any child - it changes the property on the store even if there is no implementation in the reducer</div>
<br />
</div>
<PeopleContainer people ={this.props.peopleInfo} saveAction = {this.props.save} />
</div>
)
}
})
var PeopleContainer = React.createClass({
componentWillMount(){
console.log(this.props)
this.setState({
currentChildren: this.props.people[0].children,
parentName: this.props.people[0].name
})
},
onChildChangeName(event,index){
console.log(event.target.value,index);
var newChildrenArray = this.state.currentChildren;
newChildrenArray[index].name = event.target.value
this.setState({
currentChildren: newChildrenArray
})
this.props.saveAction();
},
onParentChangeName(event){
this.setState({
parentName: event.target.value,
})
this.props.saveAction()
},
render(){
return (
<div>
Parent Name : <input value={this.state.parentName} onChange={(event) => this.onParentChangeName(event)} />
<div><br /></div>
{this.state.currentChildren.map((child, index) => {
return(<div key={index}>
Name : <input value={child.name} onChange={(event) => this.onChildChangeName(event,index)} /> <div><br /></div>
</div>)
})}
</div>
)
}
})
// Map state and dispatch to props
function mapStateToProps (state) {
return {
peopleInfo: state[0].peopleInfo,
};
}
function mapDispatchToProps (dispatch) {
return Redux.bindActionCreators({
save: save,
}, dispatch);
}
// Container components (Pass props into presentational component)
var OrgsContainer = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(OrgsContainer);
// Top-Level Component
var App = React.createClass({
render: function () {
return (
<div>
<h3>App</h3>
<OrgsContainer />
</div>
);
}
});
// Render to DOM
var Provider = ReactRedux.Provider; // Injects store into context of all descendents
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('container')
);
So it has nothing to do with React Native.
The structure on the reducer mimics the data model i am having in my app.
As can be seen in the fiddle, it is clear that we cant have the props passed to be set on state and changed there. It is evident that there is link to the store which is formed due to object references. Updating such references ends up updating the store.
Though it is a good practise not to have your props set on the state, my scenario kind of requires it. For now I have used Object.assign(), to create a new object and use this object on the state, which works for me.
I have probably missed something in the redux docs which informs about this. If anybody happens to find something , I would be glad to know it.
But i am still finding this weird.
Upvotes: 3
Views: 2788
Reputation: 1200
Ultimately, the cause of your issue is that, in your onChildChangeName
method:
var newChildrenArray = this.state.currentChildren;
newChildrenArray[index].name = event.target.value
you are mutating the same underlying object instances that your store is also referencing.
You can confirm this by adding this line to onChildChangeName
:
console.log(intialState[0].peopleInfo[0].children === newChildrenArray); // true
Solution 1: The simplest solution is that you can create a deep copy on the data when you are first setting the state, e.g.:
this.setState({
currentChildren: _.cloneDeep(this.props.people[0].children),
parentName: this.props.people[0].name
})
Since this creates a deep copy on the array, neither the array, nor the items in the array are referencing the same data as initialState
(your store), so you are now safe to change the array/object(s) without the fear of side effects.
Solution 2:
An alternative option would be to first create a shallow copy of the array, and then ensure that you create new object instances whenever you need to modify an item in the array. For example, when first calling setState
, create a new array by performing a slice()
, to ensure that changes to your component's array do not occur in the instance that is referenced by initialState
, e.g.:
this.setState({
currentChildren: this.props.people[0].children.slice(),
parentName: this.props.people[0].name
})
then in onChildChangeName
, you always create a new instance instead of mutating the existing instance, e.g.:
var newChildrenArray = this.state.currentChildren;
newChildrenArray[index] = {...newChildrenArray[index], name: event.target.value}
Though React/Redux are passing around various instances of data as you build your components, they do not perform any sort of clone/copy operation to ensure that you're using new references, so you will have to do those yourself to avoid these issues.
Upvotes: 3