Copying state in reducer is causing slowness - reactjs

I have a reducer which is giving me some slowness. I have identified the step which clones/copies a part of my state as the slow step.
export default function itemReducer(state = initialState, action) {
case ITEM_FETCH_IMPACT_UPDATE:
{
let index = action.payload.index
slow step-> let items = [...state.items];
items[index] = {...items[index], overallIsLoading: true};
return {...state, items}
};
}
items is a fairly large array of about 300 objects, with each object having ~10 properties. How should I go about speeding this up while maintaining best Redux/React practices?

That's a use case for using Immer for Writing Immutable Updates.
Writing immutable update logic by hand is frequently difficult and prone to errors. Immer allows you to write simpler immutable updates using "mutative" logic, and even freezes your state in development to catch mutations elsewhere in the app. We recommend using Immer for writing immutable update logic, preferably as part of Redux Toolkit.
With Immer drafts, or any immutable library, such code will become:
const index = action.payload.index;
state.items[index].overallIsLoading = true;
And it will patch the state and return immutable data.
See the recommendation on Redux Style Guide.

I think copying is done twice, first time when you declare items, then when you return a new state.
If splitting the state itself is not a viable option, then my suggestion is to reduce the number of copying to once:
export default function itemReducer(state = initialState, action) {
case ITEM_FETCH_IMPACT_UPDATE:
return {
...state,
items: state.items.map((item, ind) => {
if(ind===action.payload.index){
return {
...item,
overallIsLoading:true
}
} else {
return item;
}
})
}
}

If I try that code with 3000 items then it still runs in microseconds. I'm not sure you identified the problem correctly or just provided the wrong code.
const a = new Array(3000).fill('').map(() =>
(
'abcdefghijklmnopqrstuvwxyz1' +
'234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ'
)
.split('')
.reduce((result, key) => {
result[key] = Math.random();
return result;
}, {})
);
const now = performance.now();
const b = [...a]
const c = [...a]
const d = [...a]
const e = [...a]
const f = [...a]
console.log('that took:',performance.now()-now)

Related

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;
});

React: Query, delay useReducer

I've been scratching my head around this one for quite some time now, but I'm still not sure what to do.
Basically, I'm pulling data from a database via useQuery, which all works well and dandy, but I'm also trying to use useReducer (which I'm still not 100% familiar with) to save the initial data as the state so as to detect if any changes have been made.
The problem:
While the useQuery is busy fetching the data, the initial data is undefined; and that's what's being saved as the state. This causes all sorts of problems with regards to validation amd saving, etc.
Here's my main form function:
function UserAccountDataForm({userId}) {
const { query: {data: userData, isLoading: isLoadingUserData} } = useUserData(userId);
const rows = React.useMemo(() => {
if (userData) { /* Process userData here into arrays */ }
return [];
}, [isLoadingUserData, userData]); // Watches for changes in these values
const { isDirty, methods } = useUserDataForm(handleSubmit, userData);
const { submit, /* updateFunctions here */ } = methods;
if (isLoadingUserData) { return <AccordionSkeleton /> } // Tried putting this above useUserDataForm, causes issues
return (
<>
Render stuff here
*isDirty* is used to detect if changes have been made, and enables "Update Button"
</>
)
}
Here's useUserData (responsible for pulling data from the DB):
export function useUserData(user_id, column = "data") {
const query = useQuery({
queryKey: ["user_data", user_id],
queryFn: () => getUserData(user_id, column), // calls async function for getting stuff from DB
staleTime: Infinity,
});
}
return { query }
And here's the reducer:
function userDataFormReducer(state, action) {
switch(action.type) {
case "currency":
return {... state, currency: action.currency}
// returns data in the same format as initial data, with updated currency. Of course if state is undefined, formatting all goes to heck
default:
return;
}
}
function useUserDataForm(handleSubmit, userData) {
const [state, dispatch] = React.useReducer(userDataFormReducer, userData);
console.log(state) // Sometimes this returns the data as passed; most of the times, it's undefined.
const isDirty = JSON.stringify(userData) !== JSON.stringify(state); // Which means this is almost always true.
const updateFunction = (type, value) => { // sample only
dispatch({type: type, value: value});
}
}
export { useUserDataForm };
Compounding the issue is that it doesn't always happen. The main form resides in a <Tab>; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.
The quickest "fix" I can think of is to NOT set the initial data (by not calling the reducer) while useQuery is running. Unfortunately, I'm not sure this is possible. Is there anything else I can try to fix this?
Compounding the issue is that it doesn't always happen. The main form resides in a ; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.
This is likely to be expected because useQuery will give you data back from the cache if it has it. So if you come back to your tab, useQuery will already have data and only do a background refetch. Since the useReducer is initiated when the component mounts, it can get the server data in these scenarios.
There are two ways to fix this:
Split the component that does the query and the one that has the local state (useReducer). Then, you can decide to only mount the component that has useReducer once the query has data already. Note that if you do that, you basically opt out of all the cool background-update features of react-query: Any additional fetches that might yield new data will just not be "copied" over. That is why I suggest that IF you do that, you turn off the query to avoid needless fetches. Simple example:
const App = () => {
const { data } = useQuery(key, fn)
if (!data) return 'Loading...'
return <Form data={data} />
}
const Form = ({ data }) => {
const [state, dispatch] = useReducer(userDataFormReducer, data)
}
since the reducer is only mounted when data is available, you won't have that problem.
Do not copy server state anywhere :) I like this approach a lot better because it keeps server and client state separate and also works very well with useReducer. Here is an example from my blog on how to achieve that:
const reducer = (amount) => (state, action) => {
switch (action) {
case 'increment':
return state + amount
case 'decrement':
return state - amount
}
}
const useCounterState = () => {
const { data } = useQuery(['amount'], fetchAmount)
return React.useReducer(reducer(data ?? 1), 0)
}
function App() {
const [count, dispatch] = useCounterState()
return (
<div>
Count: {count}
<button onClick={() => dispatch('increment')}>Increment</button>
<button onClick={() => dispatch('decrement')}>Decrement</button>
</div>
)
}
If that works is totally dependent on what your reducer is trying to achieve, but it could look like this:
const App = () => {
const { data } = useQuery(key, fn)
const [state, dispatch] = useReducer(userDataFormReducer)
const currency = state.currency ?? data.currency
}
By keeping server state and client state separately, you'll only store what the user has chosen. The "default values" like currency stay out of the state, as it would essentially be state duplication. If the currency is undefined, you can still choose to display the server state thanks to the ?? operator.
Another advantage is that the dirty check is relatively easy (is my client state undefined?) and resets to the initial state also just mean to set the client state back to undefined.
So the actual state is essentially a computed state from what you have from the server and what the user has input, giving precedence to the user input of course.

Filtering data in a reducer then attaching it to a component with { connect } from 'react-redux';

Please correct me if I'm wrong, because I've heard different stories with using redux with react. I've heard that you should have all your logic in your reducers, I've also heard that the store should be your single source of truth.
That said, I'm taking the approach where my logic, that is filtering stuff from json file is in a reducer file. Then I call the actions to filter out different parts of the json and return it,
import Data from "./BookOfBusiness.json";
const initialState = {
TotalData: Data
};
const filterDataWTF = (state = initialState, action) => {
let newState = { ...state };
let itExport = newState.TotalData;
if (action.type === "Status55") {
let itExport22 = {...itExport};
console.log("came to Status55 and itExport value is " + itExport22); // comes undefined. WHY??
return itExport22.filter(xyz => {
if (xyz.Status55.includes("Submitted")) {
return true;
}
return false;
});
}
return itExport;
};
export default filterDataWTF;
the problem is my variable itExport22 is showing up as undefined. Any tips please. TYVM
Okay once we got something working that you can vote for an answer, lets write an answer for you.
In that case your filter is returning a boolean. You should do something like so:
return itExport22.filter(xyz => xyz.Status55.includes("Submitted"));
Returning an empty array you are sure that you are receiving the same type on your component.
Your filter is fine. Only this should work
Your reducer should always return the new state
your reducer return type is boolean or itExport22 but it's should be object contains TotalData

Do we always have to treat states as immutable in Reducer?

I'm a beginner in React & Redux and I am confused with manipulating the state in Reducers.
In most of the articles, documentations, I keep seeing that the states are immutable and we should never update the state. We should always use ...state or object.assign in the reducers
However, in famous tutorials (Cory House or other places (Eg. Here on GitHub) , they update the state the directly like the following:
var initialState = {
numberOfAjaxCall: 0
}
const ajaxStatusReducer = (state = initialState.numberOfAjaxCall, action) => {
if (action.type === AJAX_BEGIN_CALL) {
return state + 1;
}
return state;
}
Why these codes are not written like the following?
var initialState = {
numberOfAjaxCall: 0
}
const ajaxStatusReducer = (state = initialState, action) => {
if (action.type === AJAX_BEGIN_CALL) {
return {
...state,
numberOfAjaxCall: state.numberOfAjaxCall + 1
};
}
return state;
}
I would like to know whether my code is wrong or not. Or I misunderstood about Redux & Reducers or don't understand the way these codes are implemented?
Could you please help me to enlighten about these coding styles?
The first example doesn't mutate state - it returns a new number. In that case the reducer is responsible for only the one number.
If your state is shaped like in the example you gave:
var initialState = {
numberOfAjaxCall: 0
}
The reducer ajaxStatusReducer is responsible for numberOfAjaxCall only. You will still need another reducer for the overall state object, which could look something like this (simplest option, lots of other ways you could write this):
function reducer(state, action) {
return {
numberOfAjaxCall: ajaxStatusReducer(state.numberOfAjaxCall, action)
};
}
In the second example, you combine both of these reducers into one. Both are valid options and it depends on how you like to compose your code/reducers in general.

Multiple checkbox search filter using react redux

I want to create multiple checkbox search filter in my react-redux application.
I have added checkboxes, which will make request to api, but the issue is, every time when I clicked on checkbox, new data is coming from api, which is overwriting old data in the state.
How can I retain my old state ? or Is there any other way to do this ?
This is my reducer
import * as types from '../constants';
const InitialState = { data: [], };
export const dataReducer = (state= InitialState , action = null) =>
{
switch(action.type) {
case types.GET_DATA:
return Object.assign({}, state, {data:action.payload.data });
default:
return state;
}
}
It's a bit unclear in the question as to how you want to keep the old data while still fetching new data, but given that data is an array, I'll assume you mean you want to merge them.
export const dataReducer = (state= InitialState , action = null) =>
{
switch(action.type) {
case types.GET_DATA:
return Object.assign({}, state, {data: [...state.data, ...action.payload.data] });
default:
return state;
}
}
This is making the assumption that you can always just append the new data. If not you will need to be more selective about which items get merged into the array before assigning it to the state.
export const dataReducer = (state= InitialState , action = null) =>
{
switch(action.type) {
case types.GET_DATA:
let data = [...action.payload.data]
// merge in the relevant items from state.data, e.g.
for (let item in state.data.filter(it => it.shouldBeKept)) {
data.push(item)
}
return Object.assign({}, state, { data });
default:
return state;
}
}
Obviously, you will know best how to identify the items to keep or not, modify the logic hear to best suit your need. For example, it might make morse sense for you to start with let data = [...state.data] and selectively merge items from action.payload.data, or to start with an empty array and pick and choose from either arrays. The important part is that you construct a new array, and not add items to the existing array in the state.

Resources