Redux initial state gets mutated even when using Object.assign - javascript

This is a simple replication of a problem i encounter in an actual app.
https://jsfiddle.net/zqb7mf61/
Basically, if you clicked on 'Update Todo" button, the text will change from "Clean Room" to "Get Milk". "Clean Room" is a value in the initial State of the reducer. Then in my React Component, I actually try to clone the state and mutate the clone to change the value to "Get Milk" (Line 35/36). Surprisingly, the initial State itself is also mutated even though I try not to mutate it (as seen in line 13 too).
I am wondering why Object.assign does not work for redux.
Here are the codes from the jsFiddle.
REDUX
const initState = {
task: {id: 1, text: 'Clean Room'}
}
// REDUCER
function todoReducer (state = initState, action) {
switch (action.type) {
case 'UPDATE_TODO':
console.log(state)
let newTodo = Object.assign({}, state) // here i'm trying to not make any changes. But i am surpise that state is already mutated.
return newTodo
default:
return state;
}
}
// ACTION CREATORS:
function updateTodo () {
return {type: 'UPDATE_TODO'};
}
// Create Store
var todoStore = Redux.createStore(todoReducer);
REACT COMPONENT
//REACT COMPONENT
class App extends React.Component{
_onSubmit = (e)=> {
e.preventDefault();
let newTodos = Object.assign({}, this.props.todos) // here i clone the redux state so that it will not be mutated, but i am surprise that it is mutated and affected the reducer.
newTodos.task.text = 'Get Milk'
console.log(this.props.todos)
this.props.updateTodo();
}
render(){
return (
<div>
<h3>Todo List:</h3>
<p> {this.props.todos.task.text} </p>
<form onSubmit={this._onSubmit} ref='form'>
<input type='submit' value='Update Todo' />
</form>
</div>
);
}
}
// Map state and dispatch to props
function mapStateToProps (state) {
return {
todos: state
};
}
function mapDispatchToProps (dispatch) {
return Redux.bindActionCreators({
updateTodo: updateTodo
}, dispatch);
}
// CONNECT TO REDUX STORE
var AppContainer = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(App);

You use Object.assign in both the reducer as in the component. This function only copies the first level of variables within the object. You will get a new main object, but the references to the objects on the 2nd depth are still the same.
E.g. you just copy the reference to the task object around instead of actually creating a new task object.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Deep_Clone
Apart from that it would be better to not load the whole state into your component and handle actions differently. Lets just solve this for now. You will have to create a new task object in your onSubmit instead of assigning a new text to the object reference. This would look like this:
newTodos.task = Object.assign({}, newTodos.task, {text: 'Get Milk'})
Furthermore to actually update the store, you will have to edit your reducer as you now assign the current state to the new state. This new line would look like this:
let newTodo = Object.assign({}, action.todos)

Related

React-redux does not re-render on state change

I just started with redux and react-redux. I am observing a very weird behavior and not able to wrap my head around it.
I am trying something like this.
const fetchedFolders = useSelector(state=>{
console.log("redux state = ",state);
return state.fetchedFolders;
});
const updateFetchedFolders = useDispatch();
I have callback function that receives a new set of values and will update the state in store.
let appendFoldersToList=(newFolders)=>{
console.log(typeof(newFolders))
if(typeof(newFolders) === undefined)
console.log("go to error");
else{
updateFetchedFolders(setFetchedFolders([...fetchedFolders,...newFolders]));
}
}
this works perfectly and re-renders the list with new value
but if I replace the line
updateFetchedFolders(setFetchedFolders([...fetchedFolders,...newFolders]));
with
updateFetchedFolders(setFetchedFolders([...newFolders]));
it does not re-render and it still shows the old list. but in console, I can see data is updated.
I am not able to understand why it re-renders in first case and not in second case.
This is how my reducers look:-
export const reducer = (state=initialState, action)=>{
switch(action.type){
case 'SET_FOLDERS': return {
...state,
fetchedFolders:[...action.payload]
}
}
}
this is my action creator:-
export const setFetchedFolders = (payload)=>{
return {
type:'SET_FOLDERS',
payload:payload
}
}
this is my initial state:-
const initialState = {
fetchedFolders:[],
}
I don't think I am not mutating the state.
my array looks something like this::-
[
{name:cats, id:SOME_ID},
{name:dogs, id:SOME_ID}
]

Redux | Why this store's parameter type changes after second click?

I'm trying to push a new value in the store's state. It works fine the first time I click on the button "Add item", but the second time I got the following error: "state.basket.push is not a function". I configure the action to console log the state and got the following results:
1st click: {...}{basketItems: Array [ "44" ]}
2nd click: Object {basketItems: 0 }
Why the variable type is changing from array to an int?
Here is the code for the rendered component:
function Counter({ basketItems,additem }) {
return (
<div>
<button onClick={additem}>Add item</button>
</div>
);
}
const mapStateToProps = state => ({
basketItems: state.counterReducer.basketItems,
});
const mapDispatchToProps = dispatch => {
return {
additem: ()=>dispatch({type: actionType.ADDITEM, itemName:'Dummy text' }),
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
And the reducer looks like this:
import {ADDITEM} from "../actions/types";
const initialState = { basket: [], };
export default function reducer(state = initialState, action) {
switch (action.type) {
case ADDITEM:
console.log(state);
// let newBasket = state.basket.push('44');
return {
...state,
basket: state.basket.push('44')
};
default:
return state;
}
}
I'm copying the state before updating the basket to prevent weird behaviors.
There's two problems here:
state.basket.push() mutates the existing state.basket array, which is not allowed in Redux
It also returns the new size of the array, not an actual array
So, you're not doing a correct immutable update, and you're returning a value that is not an array.
A correct immutable update here would look like:
return {
...state,
basket: state.basket.concat("44")
}
Having said that, you should really be using our official Redux Toolkit package, which will let you drastically simplify your reducer logic and catch mistakes like this.

react setState seems to be cancelled by redux dispatch?

I'm trying to enable a submit button when I click a checkbox and editing this existing component to use redux (I realize it's not great, I'm just trying to get one part to work before refactoring). For now the button is not on the page.
class Survey extends Component {
state = {
questionsList: [
{
key: 'q1',
checked: this.props.survey && this.props.survey.includes('q1'),
},
{
key: 'q2',
checked: this.props.survey && this.props.survey.includes('q2'),
}
],
};
handleChange = (e) => {
const questions = JSON.parse(JSON.stringify(this.state.questionsList));
// Filter the question for the checkbox user interacted with.
const question = questions.find(q => q.key === e.target.name);
question.checked = !question.checked; // toggle
this.props.setEnableSubmit(questions.some(q => q.checked)); // dispatches a redux action (defined in mapDispatchToProps)
this.setState({ questionsList: questions });
}
render() {
return this.state.questionsList.map(question => (
<Checkbox name={question.key} onChange={this.handleChange} />
));
}
}
const mapStateToProps = (state, ownProps) => ({
survey: state.users[ownProps.match.params.userTarget].survey,
});
const mapDispatchToProps = (dispatch, ownProps) => ({
setEnableSubmit: (value) => {
dispatch(update(ownProps.match.params.userTarget, { enableSubmit: value }));
}
});
export default compose(
withRouter,
connect(
mapStateToProps,
mapDispatchToProps,
),
)(Survey);
I'm trying to store the disabled/enabledness of the button in the store, while keeping all the changes in the local state (and only persisting in the redux store on the button click)
for whatever reason, once enableSubmit happens, in re-render fn this.state.questionsList does not have the updated questionsList anymore (so I can't click the checkboxes). However it works if I remove the dispatch call. It has the same behavior if the setState is not there.
It gets to the setState without an exception. It updates enableSubmit in the store with the value passed. It doesn't seem to matter which line is called first or last or if in a cb to the dispatch or the dispatch is the cb to setState (tried all variations)
I found out that the issue is due to the component rendering this child component. The parent component had a mapStatetoProps watching a slice of the redux store that was getting updated whenever I called enableSubmit, so the parent was receiving new props, rerendering and consequently creating a new child component instance based on our setup. So the multiple mapDispatchToProps calls and seeming ignore of setState was due to a new child component re instantiating

Pass by reference assignment mutates redux state

I have an app with a menu of items, and at some point a user may edit the values of the items. When the user does so, I create a copy of the item in a seperate state branch instead of changing the original menu items. So my reducer looks like this:
const menuReducer = (state = [], action) => {
switch (action.type) {
case ADD_ITEM:
return [...state, {id: action.itemId, propA: action.itemPropA, propB: action.itemPropB}]
}
}
const editingMenuItem = (state = {}, action) => {
switch (action.type) {
case SET_EDIT_ITEM:
return {id: action.id, propA: action.itemPropA, propB: action.itemPropB}
case EDIT_ITEM:
return {id: state.id, propA: action.itemPropA, propB: action.itemPropB}
}
}
Someone selects that they want to edit an item, and this causes the dispatchEditItem thunk to trigger and create a copy in the state tree:
const dispatchEditItemThunk = itemId => (dispatch, getState) => {
const item = _.find(getState().menu, ['id', itemId]);
dispatch(setEditItem(item.id, item.propA, item.propB))
}
Then when someone wants to edit a prop, the editingThunk is dispatched:
const editingThunk = (itemId, propName) => (dispatch, getState) => {
let activeItem = getState().editingMenuItem;
// someValue is generated here
activeItem[propName] = someValue
dispatch(editItem(activeItem.propA, activeItem.propB))
}
The problem with this is that when activeItem[propName] = someValue happens, this changes the value of the item contained in the menuReducer array. I'm assuming because everything is pass by reference, and all the references lead back to the original value in the menuReducer. However, this isn't the way I would expect this to work. My assumption would be that calling getState would return a deep copy of the state, and not allow for these kinds of accidental mutations.
Is this a bug? If it isn't, is there a preferred way of writing thunks that avoids this kind of situation? In my real use case, the structure of the props in the menuItem is very complex, and it is handy to create an activeItem in the thunk and mutate it's values before dispatching to the state tree. Is doing this bad?
That's not a bug and mutating state object is highly discouraged. You can create a deep copy of an object using Object.assign and JSON.stringify methods as described here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign (Examples section).
If Redux was to create a deep copy of state on each dispatch call it could be more secure but also much slower.

Filter data according to input from child Component

I have a List of Users which I want to filter during the user Types in letters in a Textfield.
In my Child component which contains the Input field I pass the input Up via props:
onEnter(event){
console.log("ENTER")
// console.log(event.target.value)
this.props.filterEmployee(event.target.value);
}
In my Container Component I take the value
filterEmployee(val){
console.log(val)
// const allUser = this.props.allUser.allUserData;
allUser .forEach(function(user){
if(user.userNameLast.indexOf(val) != -1){
console.log(user) //works
}
});
}
The allUser is an array of data connected from my Redux-store to the Container Component.
This data are also used to display the list of Users initialzied on componentWillMount.
render() {
console.log("administration")
console.log(this.props)
const allUser = this.props.allUser.allUserData;
return (
<div id="employeeAdministration">
<h1>Mitarbeiter Verwaltung</h1>
<EmployeeAdministrationFilterList
filterEmployee={this.filterEmployee.bind(this)}
/>
{/* suchfeld - Name, Department mit checkbox*/}
<ul>
{allUser.length != 0 ? allUser.map(function (item, i) {
console.log(item)
return <li key={i}>
<UserCard
userData={item}
displayPersonalInfo={true}
showRequestDates={false}
showChangePassword={false}
/>
</li>
})
: ""
}
</ul>
</div>
)
}
}
The problem now is, that I don´t know how to tell the <UserCard /> that the data has changed. How can I pass the data from the function to the render() function?
What would be the way to go here?
EDIT:
I have also tried to go the way via the reducer, but so far it didn´t worked.
filterEmployee(val){
console.log(val)
const {dispatch} = this.props;
dispatch(filterAllUser(val));
}
And the Reducer (which is not working)
function allUser(state = {allUserData: []}, action){
switch (action.type){
case 'REQUEST_ALL_USER':
return Object.assign({}, state, {
isFetching: true
});
case 'RECEIVE_ALL_USER':
return Object.assign({}, state, {
isFetching: false,
allUserData: action.items
});
case 'FILTER_ALL_USER':
return Object.assign({}, state, {
allUserData: state.allUserData.filter(user => user.userNameLast !== action.filter )
});
default:
return state
}
}
And here is the Code how the store is connected to the component
EmployeeAdministration.PropTypes = {
allUserData: PropTypes.array
};
const mapStateToProp = state => ({
allUser: state.allUser
});
export default connect(mapStateToProp)(EmployeeAdministration)
When trying this, the result is Console output of state object
This example should be able to demonstrate a basic workflow: JSFiddle.
Basically, Redux has a one-way-dataflow. The data (here is Users in the store) is flowed from the root component to the sub-components.
Whenever you want to change the value of Users inside whichever component, you create an Action and dispatch the action to some corresponding reducer. The reducer updates the store and pass it from top to bottom.
For example, you want to filter all users whose name contains "Jo":
Action creator
Pass the Action creators into the components. An action is a plain object with format like {type: "FILTER_ALL_USERS", query: "Jo"}. Here the passing is line 73:
<Users users={this.props.users} actions={this.props.actions}></Users>
Inside the component Users, we can call this.props.actions.filter() to create an action.
Dispatch the action created
This action is automatically dispatched by redux because we have bindActionCreators in Line 93:
// Map the action creator and dispatcher
const mapDispatchToProps = (dispatch) => {
return {
actions: Redux.bindActionCreators(userActions, dispatch),
dispatch
}
}
Reducer to handle the action
All reducers will be informed about this action, but a particular one will handle it (based on its type), Line 20:
case 'FILTER_ALL_USERS':
return allUsers.filter(user => user.name.toLowerCase().indexOf(action.query.toLowerCase()) >= 0)
Re-render
The reducer will return a brand-new object as the new store, which will be passed by Redux from the root of the component. All render functions in sub-components will be called.

Categories

Resources