Redux state is mutating when editing local variable from useSelector() - reactjs

I am using useSelector() to get state and when I apply some filter to local state variable, my global state is mutating. Not sure why. Below is the code.
const filterVacations = (employees, status) => {
if(status === 'Approved') {
employees.forEach(item => {
item.vacations = item.vacations.filter(vac => vac.approved === true);
});
}
else if(status === 'Unapproved') {
employees.forEach(item => {
item.vacations = item.vacations.filter(vac => vac.approved === false);
});
}
return employees.filter(item => item.vacations.length > 0);
}
and calling this function as below:
const Vacations = () => {
const state = useSelector((state:State) => state);
const view = state.view;
const filter = state.filter;
const employees = state.employees;
employees = filterVacations(employees, filter);
return (
<>
//some component...
</>);
}
why is state is mutating here?

Its because you are not passing value when you say const filter = state.filter; but its passing reference.
For example, consider a Bookshelf having JS book on top of it in a Library. When you visit Libraria doesn't give you the copy of book but the location of the book. So if you tear page of the book and keep it same place. The librarian can aslo see torn page. But if you dont want this to happen. You need to get copy of the book and tear pages from it while the book on shelf will remain as it is.
So when you want to make the copy of the data and store it in a variable ES6 has introduced easy way to do it than earlier clumsy way. so this const filter = state.filter;
will become this const filter = [...state.filter]; // this makes copy of array

I found the issue. useSelector will return a shallow copy of the nested objects, hence the mutation. The solution is to deep copy manually.

Related

How to re-render a component when a non state object is updated

I have an object which value updates and i would like to know if there is a way to re-render the component when my object value is updated.
I can't create a state object because the state won't be updated whenever the object is.
Using a ref is not a good idea(i think) since it does not cause a re-render when updated.
The said object is an instance of https://docs.kuzzle.io/sdk/js/7/core-classes/observer/introduction/
The observer class doesn't seem to play well with your use case since it's just sugar syntax to manage the updates with mutable objects. The documentation already has a section for React, and I suggest following that approach instead and using the SDK directly to retrieve the document by observing it.
You can implement this hook-observer pattern
import React, { useCallback, useEffect, useState } from "react";
import kuzzle from "./services/kuzzle";
const YourComponent = () => {
const [doc, setDoc] = useState({});
const initialize = useCallback(async () => {
await kuzzle.connect();
await kuzzle.realtime.subscribe(
"index",
"collection",
{ ids: ["document-id"] },
(notification) => {
if (notification.type !== "document" && notification.event !== "write")
return;
// getDocFromNotification will have logic to retrieve the doc from response
setDoc(getDocFromNotification(notification));
}
);
}, []);
useEffect(() => {
initialize();
return () => {
// clean up
if (kuzzle.connected) kuzzle.disconnect();
};
}, []);
return <div>{JSON.stringify(doc)}</div>;
};
useSyncExternalStore, a new React library hook, is what I believe to be the best choice.
StackBlitz TypeScript example
In your case, a simple store for "non state object" is made:
function createStore(initialState) {
const callbacks = new Set();
let state = initialState;
// subscribe
const subscribe = (cb) => {
callbacks.add(cb);
return () => callbacks.delete(cb);
};
// getSnapshot
const getSnapshot = () => state;
// setState
const setState = (fn) => {
state = fn(state);
callbacks.forEach((cb) => cb());
};
return { subscribe, getSnapshot, setState };
}
const store = createStore(initialPostData);
useSyncExternalStore handles the job when the update of "non state object" is performed:
const title = React.useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().title
);
In the example updatePostDataStore function get fake json data from JSONPlaceholder:
async function updatePostDataStore(store) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${Math.floor(Math.random()*100)+1}`)
const postData = await response.json()
store.setState((prev)=>({...prev,...postData}));
};
My answer assumes that the object cannot for some reason be in React as state (too big, too slow, too whatever). In most cases that's probably a wrong assumption, but it can happen.
I can't create a state object because the state won't be updated whenever the object is
I assume you mean you can't put that object in a React state. We could however put something else in state whenever we want an update. It's the easiest way to trigger a render in React.
Write a function instead of accessing the object directly. That way you can intercept every call that modifies the object. If you can reliably run an observer function when the object changes, that would work too.
Whatever you do, you can't get around calling a function that does something like useState to trigger a render. And you'll have to call it in some way every time you're modifying the object.
const myObject = {};
let i = 0;
let updater = null;
function setMyObject(key, value) {
myObject[key] = value;
i++;
if (updater !== null) {
updater(i);
}
};
Change your code to access the object only with setMyObject(key, value).
You could then put that in a hook. For simplicity I'll assume there's just 1 such object ever on the page.
function useCustomUpdater() {
const [, setState] = useState(0);
useEffect(()=>{
updater = setState;
return () => {
updater = null;
}
}, [setState]);
}
function MyComponent() {
useCustomUpdater();
return <div>I re-render when that object changes</div>;
}
Similarly, as long as you have control over the code that interacts with this object, you could wrap every such call with a function that also schedules an update.
Then, as long as your code properly calls the function, your component will get re-rendered. The only additional state is a single integer.
The question currently lacks too much detail to give a good assessment whether my suggested approach makes sense. But it seems like a very simple way to achieve what you describe.
It would be interesting to get more information about what kind of object it is, how frequently it's updated, and in which scope it lives.

Redux: A state mutation was detected inside a dispatch

I realise there are probably a million and one different causes for this error but I am struggling to find the cause of mine. I am relatively new to Redux and state handling in the Reducer and have learned from a few examples online to come up with the code sample below which is called within the Reducer:
const updateTripsWhereVenueDeleted = (state, action) => {
debugger;
const deletedVenueId = action.venue.Id;
const originalTrips = [...state];
const newTrips = originalTrips.map((trip) => {
if (trip.VenueId === deletedVenueId) {
trip.VenueId = 0;
trip.VenuePegId = 0;
}
return trip;
});
debugger;
return [...newTrips];
};
I have state which is an array of Trips:
And Action contains a 'Venue' record.
Basically, the Venue being passed was deleted and I want to update the relevant fields on all Trips that reference the deleted Venue.
When I run the above code in the Reducer things seem to go in the right direction until the browser crashes with the following error:
Unhandled Rejection (Invariant Violation): A state mutation was
detected inside a dispatch, in the path: trips.0.VenueId. Take a
look at the reducer(s) handling the action
{"type":"UPDATE_TRIPS_VENUE_DELETED","venue": {.... the object}
UPDATE_TRIPS_VENUE_DELETED is the action.type that calls my method above.
Is there an obvious mis-use of handling (spread) arrays in a state or something. I feel this should be an easy thing to do but nothing I have tried has so far worked correctly.
Spreading an object or array ([...state] here) does not break nested references. So you are mutating state by not making a copy of the nested object within your map -- trip.VenueId = 0;.
This leads to another observation, map returns a new array, so this negates the need to use originalTrips altogether. It is just as safe to do state.map(). The final spread [...newTrips] is definitely unnecessary as well.
To fix your mutation create a clone of the objects to be updated:
const updateTripsWhereVenueDeleted = (state, action) => {
const deletedVenueId = action.venue.Id;
const newTrips = state.map((trip) => {
if (trip.VenueId === deletedVenueId) {
// Copy other trip values into NEW object and return it
return { ...trip, VenueId: 0, VenuePegId: 0 };
}
// If not matched above, just return the old trip
return trip;
});
return newTrips;
};
Spread operator ... only does shallow copy. If the array is nested or multi-dimensional, it won't work.
There are plenty of way we can do this, below are some of them
1] Deep copy using JSON
const originalTrips = JSON.parse(JSON.stringify(state));;
2]
const newTrips = originalTrips.map((trip) => {
if (newTrip.VenueId === deletedVenueId) {
let newTrip = Object.assign({},trip);
newTrip.VenueId = 0;
newTrip.VenuePegId = 0;
return newTrip;
}
return trip;
});

Reactjs: Unknown why function re-run second time

I started learning react, yesterday I ran into this issue, somebody please explain me.
When I click button "add to wishlist" the piece of code below run 1st time, set product property "inWishList": true, but unknown why it rerun and set it back to "false" value.
const AddToWishList = (id) => {
setProducts((prev) => {
const latest_Products = [...prev];
const selected_Prod_Id = latest_Products.findIndex((x) => x.id === id);
latest_Products[selected_Prod_Id].inWishList =
!latest_Products[selected_Prod_Id].inWishList;
console.log(latest_Products);
return latest_Products;
});
};
_ The piece of code below works perfect, run only 1 time, however, i don't understand the difference between 2 codes
const AddToWishList = (id) => {
setProducts((currentProdList) => {
const prodIndex = currentProdList.findIndex((p) => p.id === id);
const newFavStatus = !currentProdList[prodIndex].inWishList;
const updatedProducts = [...currentProdList];
updatedProducts[prodIndex] = {
...currentProdList[prodIndex],
inWishList: newFavStatus,
};
console.log(updatedProducts);
return updatedProducts;
});
};
In the first snippet you are mutating the state object when toggling the inWishList property.
const AddToWishList = (id) => {
setProducts((prev) => {
const latest_Products = [...prev];
const selected_Prod_Id = latest_Products.findIndex((x) => x.id === id);
latest_Products[selected_Prod_Id].inWishList =
!latest_Products[selected_Prod_Id].inWishList; // <-- state mutation
console.log(latest_Products);
return latest_Products;
});
};
The second snippet you not only shallow copy the previous state, but you also shallow copy the element you are updating the inWishList property of.
const AddToWishList = (id) => {
setProducts((currentProdList) => {
const prodIndex = currentProdList.findIndex((p) => p.id === id);
const newFavStatus = !currentProdList[prodIndex].inWishList;
const updatedProducts = [...currentProdList]; // <-- shallow copy state
updatedProducts[prodIndex] = {
...currentProdList[prodIndex], // <-- shallow copy element
inWishList: newFavStatus,
};
console.log(updatedProducts);
return updatedProducts;
});
};
Now the reason these two code snippets function differently is likely due to rendering your app into a StrictMode component.
StrictMode
Specifically in reference to detecting unexpected side effects.
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
When you pass a function to setProducts react actually invokes this twice. This is exposing the mutation in the first example while the second example basically runs the same update twice from the unmutated state, so the result is what you expect.

How to add another key value pair to array of object coming from the api

I am trying to add another property to the array of object coming from the api.
const productList = useSelector((state) => state.productReducer.productList);
if (productList !== undefined) {
for (let b = 0; b < productList.length; b++) {
productList[b].power = 0;
}
}
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
async function fetchData() {
setLoading(true);
await dispatch(fetchProductList());
setLoading(false);
}
fetchData();
});
return unsubscribe;
}, [navigation]);
As you can see I am getting data by using using useDispatch, useSelector hooks.
But What I am doing now is looping the product list and adding power property to each individual product.
This thing is that this process is recalled every rendered.
If I changed the productList[4].power = 4 in jsx.
This loop is called again and the data is gone back to 0.
if (productList !== undefined) {
for (let b = 0; b < productList.length; b++) {
productList[b].power = 0;
}
}
Where should I put this code so that It will not re-rendered.
For my case I trying putting this loop in useEffect but the initial data is empty array so it won't work
It would help if you provided the whole file. It's difficult to see exactly what the JSX is referencing and changing however....
I can see that you're dispatching an action to get some data which is then stored in your redux store and brought into the component with the useSelector hook.
Now first things first... It is possible that data mutations can occur when using useSelector hook, meaning if you're not handling immutability then changes to this state object from a component could be modifying the data in your store.
Regardless of this, your current implementation shows that regardless of what is in the store, after you get the products from the store, you are setting every power to 0 having that loop beneath your selector hook. So whatever you do it will always show as 0.
Perhaps in your reducer when saving the products I would to something like below. Where data is your product list api response...
case 'FETCH_PRODUCT_LIST_SUCCESS':
const newState = _.clone(state);
newState.productList = data.map(product =>
if (product.power === undefined || product.power === null) {
product.power = 0;
}
return product;
);
return newState;
For the useSelector hook, I would highly recommend creating functions that take state as a parameter which returns a copy of your data using a library like 'lodash'. In the example below you can see that I'm using the .clone function on lodash which ensures that the data passed to your component will not carry the same reference. Meaning your components will not be able to modify the data in the store.
import _ from 'lodash-es';
export const getProductList = (state) => _.clone(state.productReducer.productList);
Then use in your selector hook like so:
const productList = useSelector(state => getProductList(state));
This will help to keep your code clean and easy to understand.
For the rest of your task, there are many ways that you can approach this but I'll give you two examples.
Create an action for updating a product's power which will be
dispatched by the component when the power input changes. This will
persist the change in the redux store. In your reducer you would add
a case statement for this new action and modify the data as you
wish. You may even want to persist this change in a database
somewhere and instead make an api call to save a product's power and
then modify your store's product list with the newly persisted
power.
If you only want to persist these changes locally in the
component use can make use of the useState hook. You could initialise a useState hook with the products from your product list selector and write a handler to update this local state.
Option 1 would look like this example below. Where index and power would be passed in your updateProductPower action's payload.
case 'UPDATE_PRODUCT_POWER':
const newState = _.clone(state);
newState.productList[index].power = power;
return newState;
Option 2 would look like this:
const productList = useSelector((state) => _.clone(state.productReducer.productList));
const [products, setProducts] = useState(productList);
// Updates your local state if the store changes. But will overwrite changes in your local state.
useEffect(() => {
setProducts(productList);
}, [productList]);
// Dispatches an action to persist changes on API when the local products state changed.
useEffect(() => {
dispatch(persistProductList(products);
}, [products]);
const handlePowerChange = (index, power) => {
const result = _.clone(products);
result[index].power = power;
setProducts(result);
}
You can use a mixture of both but you just need to remember how data flows in a redux/flux architecture.
Sorry not a perfect answer but should help you understand the direction you need to go. I have to crack on with work now.
You can try the split operator:
if (productList !== undefined) {
for (let b = 0; b < productList.length; b++) {
productList[b] = {...productList[b], power: 0};
}
}
Or you can assign via array brackets:
if (productList !== undefined) {
for (let b = 0; b < productList.length; b++) {
productList[b]["power"] = 0;
}
}

Having to dynamically change the state : how to avoid an infinite-loop?

I have a React component which receives data from a parent.
Now, this data is dynamic and I do not know beforehand what the properties are called exactly.
I need to render them in a certain fashion and that all works perfectly fine.
Now though, these dynamic objects have a property which is a number, which has to be displayed in my component.
To do so, I thought while iterating over the data, I will add the values to the sum, which is to be displayed. Whenever one of the data object changes, the sum will change, too (since I am using useState and React will detect that change.
But that exactly is the problem I don't know how to solve.
It is obvious that right now my code generates an infinite-loop:
The component is created and rendered for the first time.
During this process, setSum() is called, and therefore changing the state.
React detects that and orders a re-rendering.
So how do I fix this? I feel like I am missing something quite obvious here, but I am too invested to see it.
I have tried to boil down my code to the most easy to read code snippet which focuses on the problem only. Any suggestions to improve the readabilty are welcome!
const ComponentA = (data) => {
const [sum, setSum] = React.useState(0);
const renderData = (dataToRender) => {
//Here lies the problem already
setSum(0)
const result = [];
dataToRender.forEach((objData, index) => {
result.push(<JSX Item>Content</JSX Item>);
//and some more stuff, not relevant
// will not get this far
const newSum = sum+objData.propertyAmount;
setSum(newSum);
});
return result;
};
return(
//...someJSXElements
{data.relevantObjectArray && renderData(data.relevantObjectArray)}
<div>{sum}</div>
);
}
The reason it's re-rendering infinitely is because you are setting the state every time the component is rendered, which subsequently triggers another re-render. What you need to do is separate your display code from your state-setting code. I initially thought that useEffect would be a good solution (you can see the edit history for my original answer), however from the React docs:
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. (React docs)
So you could therefore try something like this:
const reducer = (sum, action) => {
switch (action.type) {
case "increment":
return sum + action.propertyAmount;
default:
throw new Error();
}
};
const initialSum = 0;
const ComponentA = (data) => {
const [sum, sumReducer] = React.useReducer(reducer, initialSum);
React.useEffect(() => {
data.relevantObjectArray.forEach((objData, index) => {
sumReducer({ type: "increment", propertyAmount: objData.propertyAmount });
});
}, [data.relevantObjectArray]);
const renderData = (dataToRender) => {
const result = [];
dataToRender.forEach((objData, index) => {
result.push(<div>Content</div>);
});
return result;
};
return (
<div>
{data.relevantObjectArray && renderData(data.relevantObjectArray)}
<div>{sum}</div>
</div>
);
};
Example on codesandbox.io.
const ComponentA = (data) => {
const [sum, setSum] = React.useState(0);
const renderData = (dataToRender) => {
//Here lies the problem already
setSum(0)
const result = [];
dataToRender.forEach((objData, index) => {
result.push(<JSX Item>Content</JSX Item>);
//and some more stuff, not relevant
// will not get this far
const newSum = sum+objData.propertyAmount;
setSum(newSum);
});
return result;
};
React.useEffect(() => {
renderData();
return () => {
console.log('UseEffect cleanup')});
}, [data);
return(
//...someJSXElements
//The line below is causing the continuos re-render because you keep calling the function (renderData)
//{data.relevantObjectArray && renderData(data.relevantObjectArray)}
<div>{sum}</div>
);
}

Resources