React : reducer isn't updating my state when using Object.assign - reactjs

I use a reducer to update this state :
const [playlist, dispatch] = useReducer(jspfReducer,new playlistModel());
It uses my playlistModel class, which is just a wrapper that adds some methods to manipulate easily my data - I need it.
I want to update the state, but avoid as much possible unecessary renders.
So when calling the reducer case UPDATE_JSPF_TRACK; I update only the matching track of the playlistModel track array.
function jspfReducer(state, action){
switch (action.type) {
case 'UPDATE_JSPF_TRACK':
const [index,jspf] = action.payload;
//update only that single track.
const newTracks = state.track.map(
(track, i) => i === index ? new trackModel(jspf) : track
);
const newState = Object.assign(
state,
{
track:newTracks
}
);
//my object value is correctly updated here :
console.log("NEW STATE",newState);
return newState;
break;
}
};
The value logged in the console is correctly updated.
But in my provider, the state update is not detected:
export function PlaylistProvider({children}){
const [playlist, dispatch] = useReducer(jspfReducer,new playlistModel());
//when state updates
useEffect(()=>{
console.log("PLAYLIST HAS BEEN UPDATED!!!",playlist);//does not fire
},[playlist])
What is wrong and how could I fix this ?
Thanks !

You'll need to create a new object for the state changes to be detected:
const newState = Object.assign(
{},
state,
{
track: newTracks,
},
);
or
const newState = { ...state, track: newTracks };
The way React works is it detects if the state objects have different referential equality, which basically means that if you only modify an object's attributes, it will still be considered the same object for the purposes of rendering. To trigger state change effect you'll need to create a new object.

Related

Redux reducer doesn't cause re-render after mapping to new array or using immer

In my redux reducer I map over state and copy internal items, then return the list, the list should be a new reference so the reducer should cause a re-render on change, but it doesn't.
The code below does not cause a re-render.
const initialState: Group[] = [];
export default function activeGroups(state = initialState, action: AnyAction) {
switch (action.type) {
case 'groups/createPod/fulfilled': {
// append the new pod into the active group that matches the groupId
const { pod, groupId } = action.payload;
const newState = state.map((group) => { // Map to new list
if (group.id === groupId) {
return {
...group, // Not mutating original state, copying the state into new list
pods: [pod, ...group.pods],
};
}
return group;
});
return newState; // This does not cause a re-render, why?
}
I've tried produce from immer
case 'groups/createPod/fulfilled': {
// append the new pod into the active group that matches the groupId
const nextState = produce(state, (draft) => {
const group = draft.find((e) => e.id === action.payload.groupId);
if (group) {
group.pods.unshift(action.payload.pod);
}
});
return JSON.parse(JSON.stringify(nextState)); // Even tried this
}
Figured it out. Redux is re-rendering as normal. The problem was unrelated to Redux.
It was because the object I was trying to update was being passed as a navigation param in react native, and this does not update with state changes (the reference is to the object passed through navigation, not to the Redux state).
Using useSelector() solves it.

Tell React/Redux to ignore object references and just do a deep equality check

Problem - my component is updating even if the input data hasn't changed and I suspect it's because an object ref change while a deep compare didn't change.
I have a reducer like so:
const getDefaultState = () => ({
mainNotifMessage: '(unknown message)',
notifDetails: '(unknown notification details)',
snackBarOpen: true
});
export type CPAction = {type: string, val: object}
export const notifsReducer = (state = getDefaultState(), action: CPAction) => {
switch (action.type) {
case c.NOTIFY_ERROR:
return {
...state,
...action.val
};
default:
return {
...state
};
}
};
and then in an component I have:
function mapStateToProps(state: RootState) {
return {
notifs: state.notifs
};
}
I think what happens is that state.notif always gets a new object reference, so React/Redux is always updating my component even if there are no changes. But if I change it to this:
function mapStateToProps(state: RootState) {
return {
mainNotifMessage: state.notifs.mainNotifMessage,
snackBarOpen: state.notifs.snackBarOpen,
severity: state.notifs.severity,
notifDetails: state.notifs.notifDetails
};
}
then it doesn't update the components. Is there a way to tell Redux/React to always to do a deep compare and not return early on object reference differences?
I think what happens is that state.notif always gets a new object
reference, so React/Redux is always updating my component even if
there are no changes.
By default, React-Redux decides whether the contents of the object returned from mapStateToProps are different using === comparison (a "shallow equality" check) on each fields of the returned object.
default:
return {
...state
};
You are returning a new reference each time in your default state.
So when you read values using dot notation, you get the same values.
But for Javascript, it is a new object reference, hence React-Redux will try to re-render the component.
You can just do return state instead of spreading it and creating a new object reference in the default case of your reducer.
Reference : https://react-redux.js.org/using-react-redux/connect-mapstate#return-values-determine-if-your-component-re-renders

Redux state is not updating into the react component

How the redux state is updated into the react component?
I tried using initial state and not mutating the object and return that
Redux
const initialState = {
filteredProviderData:[],
filteredAlsoSpeaksData:[],
filteredOfficeHours:[]
};
function reducer(state = initialState, action = {}) {
switch (action.type) {
case HANDLE_FILTER_CHANGE:
let filteredProviderData = '';
let filteredAlsoSpeaksData='';
let filteredOfficeHours = ''
return {...state, filteredProviderData,filteredAlsoSpeaksData,filteredOfficeHours};
case RESET_FILTER_COLLECTION:
// RESET the Array as shown into the HANDLE_FILTER_CHANGE
}}
React component
const mapStateToProps = state => {
return {
filteredProviderData:state.providerList && state.providerList.filteredProviderData,
filteredAlsoSpeaksData:state.providerList && state.providerList.filteredAlsoSpeaksData,
filteredOfficeHours:state.providerList && state.providerList.filteredOfficeHours
}}
Here my question is how to update the array which is into the HANDLE_FILTER_CHANGE
RESET_FILTER_COLLECTION
I need to update array based on some condition and return that updated array
Here into the based on the condition only one array will be updated and return that only two would remains same.
You need to change the local state once you updated the redux store. this.setState({loading:true}) Kind of approach will work and it works for me too.

How to use redux saga in editable table efficiently

I have a multi page react application in which one endpoint has to show data in tabular form. Show I take GET_INFO action on componentWillMount of that endpoint. Now I have a reducer called table_info which has table_data array and shouldTableUpdate boolean in it.
My table is editable with edit and delete icon in every row. I am facing problem in update, on update I call reducer with action UPDATE_TABLE_ROW and if success than I do something like following :
//reducer.js
const initialState = {
table_data:{}, shouldTableUpdate:false;
}
export default function myReducer(state=initialState, action){
switch(action.type){
case UPDATE_SUCCESS:
// how to handle edited row here?
// also when I print my state of this reducer
// state becomes nested, so if one does lots of updates
// it will be become very heavy...
return {...state, shouldTableUpdate:true}
}
}
Can you tell how to handle update, delete, add on table using redux saga efficiently ? On googling I get naive examples only, so came to SO.
Note: Can't show the actual code as it's for my company project. Sorry for that.
Thanks.
Can you tell how to handle update, delete, add on table using redux saga efficiently ?
Well you can plainly manipulate the state object using a reducer only.
Comments:
table_data is a list and not an object.
I don't think you'll be needing shouldTableUpdate since state change in store will trigger a component update if state field is mapped in mapStateToProps.
So here's a basic template of adding, updating and deleting items via reducer.
const initialState = {
table_data: [],
};
export default function myReducer(state=initialState, action){
switch(action.type) {
case ADD_ITEM:
return {
...state,
table_data: [
...state.table_data,
action.item, // item to be added
]
};
case UPDATE_ITEM:
let updatedItem = action.item;
// do something with updatedItem
return {
...state,
table_data: table_data.map(e => (
e.id === updatedItem.id ? updatedItem : e
)),
};
case DELETE_ITEM:
const index = state.table_data.findIndex(e => e.id === action.item.id);
const numItems = state.table_data.length;
return {
...state,
table_data: [
// exclude index
...table_data.slice(0, index),
...table_data.slice(index+1, numItems),
]
};
default:
return state;
}
}

Getting error in react-redux reducer unexpected

I thought I had safely created a new state and returned with the following code inside my Redux reducer. Basically, I need to change one record inside my state.data array and return the new state. My expectation was that since newState is completely new that I was good.
After installing redux-immutable-state-invariant, I'm seeing that I still have a problem (code and error below).
let newState = Object.assign({}, state);
let newData = [];
newState.data.map(function(rec){
if (rec.id === sessionIdToUpdate) {
rec.interestLevel = newInterestLevel;
newData.push(rec);
} else {
newData.push(rec);
}
});
newState.data = newData;
return newState;
and the error...
A state mutation was detected inside a dispatch, in the path: `sessions.data.0.interestLevel`
If I comment out the line
rec.interestLevel = newInterestLevel;
the error goes away, but then so does the purpose of this reducer.
That's because Object.assign({}, state) creates a shallow copy, and data and all its rec objects are shared with the previous state object. If you assign to one of them, you mutate the old state.
Instead of mutating the rec, you can specify the new state declaratively with something like this:
const newState = {
...state,
data: state.data.map((rec) =>
rec.id === sessionIdToUpdate ? {...rec, interestLevel: newInterestLevel} : rec
)
};

Resources