Redux updating nested data [Immutable Update Patterns] - reactjs

Can anyone help with this update pattern. I am not using any libraries like immer.
I have to update a nested object and the data looks like dis
Sample data
{
isFetching: false
data:{
nba : {
stack :{
1:[]
}
}
}
}
My Reducer
{
...state,
isFetching: false,
data: {
...state.data,
[action.payload.team]: {
...state[action.payload.team],
[action.payload.framework]: {
...state[action.payload.framework],
[action.payload.build]: action.payload.resp
}
}
}
};
I am able to update until second level but unable to update third child.
can anyone throw a light on where i am missing it.
I put a demo on codesandbox.
https://codesandbox.io/s/todos-0ygrs
Click on collapse and inner collapse items. I am logging the changes for the state in the console below. As you can see at last level, build numbers are getting replaced with the new one's.
Current Behaviour After you expand nba and all the three childs
{
nba: {
stack:{
3:[]
}
}
Expected Behaviour: After you expand stack and all the three childs
{
nba: {
stack:{
1:[],
2:[],
3:[]
}
}
}

You probably have to use a get helper because you may try to set a part of state that doesn't exist yet.
With the get helper you can set the state like this:
const { team, framework, build, resp } = action.payload;
const newState = {
...state,
isFetching: false,
data: {
...get(state, ['data']),
[team]: {
...get(state, ['data', team]),
[framework]: {
...get(state, ['data', team, framework]),
[build]: resp,
},
},
},
};

Somehow i figured out my mistake, Hope it helps someone in future
Initial state should not be null, it should be empty object and update pattern should be in this manner
{
...state,
isFetching: false,
data: {
...state.data,
[action.payload.team]: {
...state.data[action.payload.team],
[action.payload.framework]: {
...state.data[action.payload.team][action.payload.framework],
[action.payload.build]: action.payload.resp
}
}
}
};
if it fails, then try this way
let teamTemp = { ...state.data[action.payload.team]}
{
...state,
isFetching: false,
data: {
...state.data,
[action.payload.team]: {
...teamTemp ,
[action.payload.framework]: {
...teamTemp[action.payload.framework],
[action.payload.build]: action.payload.resp
}
}
}
};
I have forked my codesandbox and updated latest code.
Old Code: https://codesandbox.io/s/todos-0ygrs
New Code: https://codesandbox.io/s/todos-zqeki

Related

Unable to filter data in Redux Reducer

I'm trying to create a reducer for a ToDo App where i have successfully managed to create an Add reducer but somehow i'm unable to filter data on the basis of the ID suppose my ID is 1 then if there is a ToDo Bucket(bucket is basically a parent where i'm creating ToDo's) then get that ToDo bucket data.
In Redux dev tools i have checked it is showing me a infinite hierarchical tree of filteredData object. but not showing directly the Bucket ID 1 data and same with delete reducer as well. Can someone please help in highlighting what exactly wrong i'm doing or missing.
case EDIT_TODO_BUCKET: {
return (
{
...state,
data: {
...state.data,
[action.payload.id]: {
...state.data[action.payload.id],
filteredData: state.data[action.payload.id]
}
}
}
)
}
case DELETE_TODO_BUCKET: {
return (
{
...state,
data: {
...state.data,
[action.payload.id]: {
...state.data[action.payload.id],
}
}
}
)
}
case DELETE_TODO_BUCKET: {
return (
{
...state,
data: {
...state.data,
[action.payload.id]: {
...state.data[action.payload.id],
}
}
}
)
}
^ Read this It doesn't do anything at all except create a new reference each time.
I imagine you might want the following:
return {
...state,
// removes the key of action.payload.id from the state.data
data: Object.entries((acc,[key,value]) => key === action.payload.id ? acc : ({...acc,[key]:value}),{})
}

Updating a single value for each item in object in Redux State

I have a list of notifications inside redux state. As you can see below.
There could be more items in the list, and my goal is to set isRead value to true for all items in the list depending on the isDiscussionType value. I am using the following code in the reducer:
case MARKALLASREAD_NOTIFICATIONS_SUCCESS:
return {
...state,
loading: false,
notifications:
Object.keys(state.notifications).map(id => {
if (state.notifications[id].isDiscussionType == action.payload.isDiscussionType)
return { ...state.notifications[id], isRead: true }
else
return { ...state.notifications[id] }
})
};
This code results in the following state, where the key is lost (set to 0), and the isRead value is not changed (although the database is updated correctly).
Do you see any problems in the code I shared above?
Map returns an array instead of an object. That is why you lose your id, since the 0 is just the index within the array, which is now under notifications. I would move the generation of notifications out of the return to get more flexibility:
case MARKALLASREAD_NOTIFICATIONS_SUCCESS:
const notifications = { ...state.notifications }
Object.values(notifications).forEach(notification => {
if(notification.isDiscussionType === action.payload.isDiscussionType) {
notifications[notification.id] { ...notification, isRead: true }
}
}
return {
...state,
loading: false,
notifications
};
This will return an immutable object with each notification changed, if the isDiscussionType are the same. Because map, filter, reduce return arrays, I would move it out of the return and use forEach.
Hope this helps. Happy coding.
If your notifications is a list i.e. an array, you shouldn't use Object.keys on it, and go straight with the map:
case MARKALLASREAD_NOTIFICATIONS_SUCCESS:
return {
...state,
loading: false,
notifications: state.notifications.map(notification => {
if (notification.isDiscussionType === action.payload.isDiscussionType) {
return {
...notification,
isRead: true
}
} else {
return notification;
}
})
};

Update the part of the redux store only if the value is not null

I have the following code in my reducer. It works fine except that when comment.parentPostId is null. I wonder how I can run [comment.parentPostId] part only when comment.parentPostId is not null.
As a solution, I can define two reducers, one per each condition. But, it does not seem like a solid approach. Any suggestions?
case POST_COMMENT_SUCCESS:
const comment = action.payload.normalizedData;
return {
...state,
loading: false,
editing: false,
alignPerspectives: {
...state.alignPerspectives,
[comment.submissionId]: {
...state.alignPerspectives[comment.submissionId],
discussionPosts: [...state.alignPerspectives[comment.submissionId].discussionPosts, comment.id]
}
},
discussionPosts: {
...state.discussionPosts,
[comment.id]: comment,
[comment.parentPostId]: {
...state.discussionPosts[comment.parentPostId],
childPosts: [...state.discussionPosts[comment.parentPostId].childPosts, comment.id]
}
}
};
You can always split your reducer and delgate some part of the process to the other reducer as mentioned here https://redux.js.org/recipes/structuring-reducers/splitting-reducer-logic
OR
You can use a ternary operator to check if it's a valid value and return the state object accordingly
case POST_COMMENT_SUCCESS:
const comment = action.payload.normalizedData;
return comment.parentPostId ? {
...state,
loading: false,
editing: false,
alignPerspectives: {
...state.alignPerspectives,
[comment.submissionId]: {
...state.alignPerspectives[comment.submissionId],
discussionPosts: [...state.alignPerspectives[comment.submissionId].discussionPosts, comment.id]
}
},
discussionPosts: {
...state.discussionPosts,
[comment.id]: comment,
[comment.parentPostId]: {
...state.discussionPosts[comment.parentPostId],
childPosts: [...state.discussionPosts[comment.parentPostId].childPosts, comment.id]
}
}
} : {
...state,
loading: false,
editing: false,
alignPerspectives: {
...state.alignPerspectives,
[comment.submissionId]: {
...state.alignPerspectives[comment.submissionId],
discussionPosts: [...state.alignPerspectives[comment.submissionId].discussionPosts, comment.id]
}
},
discussionPosts: {
...state.discussionPosts,
[comment.id]: comment,
}
}
Hope this helps !

Redux overwrites model with previous state

I am currently making a sample project in AngularJs combined with Redux.
I am struggling to get the mappings from the reducer working.
I have a simple input where users can set a new name together with a drop down to select a 'company'.
<input type="text" ng-model="$ctrl.single.object.name">
<select ng-change="$ctrl.getProperties()"
ng-options="option.description as option.description for option in $ctrl.list.all"
ng-model="$ctrl.single.object.company">
When the user changes the company, new properties need to be fetched in order for the user to set these properties.
function FooController($ngRedux, FooActions, BarActions) {
this.$onInit = function () {
this.unsubscribeCompanies = $ngRedux.connect(this.mapStateToThis, BarActions)(this);
this.fetchCompanyList();
};
this.$onDestroy = function () {
this.unsubscribeCompanies();
};
this.fetchCompanyList = function () {
this.fetchCompanies().payload.then((response) => {
this.fetchCompaniesSuccess(response.data);
}, (error) => {
this.fetchCompaniesError(error.data);
});
};
this.getProperties = function () {
this.fetchCompanyProperties(this.single.object.company).payload.then((response) => {
this.fetchCompanyPropertiesSuccess(response.data);
}, (error) => {
this.fetchCompanyPropertiesError(error.data);
});
};
this.mapStateToThis = function (state) {
return {
list: state.bar.list,
single: state.bar.single
};
};
}
module.exports = {
template: require('./index.html'),
controller: ['$ngRedux', 'FooActions', 'BarActions', FooController]
}
The problem I get is that the name and the selected company are overwritten with empty values when the fetch for properties is successful. I get why the values are overwritten with empty values and I have found a way to get it working.
export const GET_COMPANIES = 'GET_COMPANIES';
export const GET_COMPANIES_SUCCESS = 'GET_COMPANIES_SUCCESS';
export const GET_COMPANIES_ERROR = 'GET_COMPANIES_ERROR';
export const GET_COMPANIES_PROPERTIES = 'GET_COMPANIES_PROPERTIES';
export const GET_COMPANIES_PROPERTIES_SUCCESS = 'GET_COMPANIES_PROPERTIES_SUCCESS';
export const GET_COMPANIES_PROPERTIES_ERROR = 'GET_COMPANIES_PROPERTIES_ERROR';
export default function BarActions($http) {
function fetchCompanies() {
return {
type: GET_COMPANIES,
payload: $http.get('api/companies')
};
}
function fetchCompaniesSuccess(companies) {
return {
type: GET_COMPANIES_SUCCESS,
payload: companies
};
}
function fetchCompaniesError(error) {
return {
type: GET_COMPANIES_ERROR,
payload: error
};
}
function fetchCompanyProperties(company) {
return {
type: GET_COMPANIES_PROPERTIES,
payload: $http.get(`api/company/${company}/properties`)
};
}
function fetchCompanyPropertiesSuccess(properties) {
return {
type: GET_COMPANIES_PROPERTIES_SUCCESS,
payload: properties
};
}
function fetchCompanyPropertiesError(error) {
return {
type: GET_COMPANIES_PROPERTIES_ERROR,
payload: error
};
}
return {
fetchCompanies,
fetchCompaniesSuccess,
fetchCompaniesError,
fetchCompanyProperties,
fetchCompanyPropertiesSuccess,
fetchCompanyPropertiesError
}
}
The way I overwrite the values in the reducer is as follows:
import { GET_COMPANIES, GET_COMPANIES_SUCCESS, GET_COMPANIES_ERROR, GET_COMPANIES_PROPERTIES, GET_COMPANIES_PROPERTIES_ERROR, GET_COMPANIES_PROPERTIES_SUCCESS } from "../actions/bar.actions";
const all = [];
const initialState = {
list: {
all,
filtered: all,
error: null,
loading: false
},
single: {
object: {},
error: null,
loading: false
}
};
export function BarReducer(state = initialState, action) {
switch (action.type) {
case GET_COMPANIES:
return { ...state, list: { all: [], filtered: [], error: null, loading: true } };
case GET_COMPANIES_SUCCESS:
return { ...state, list: { all: action.payload, filtered: action.payload, error: null, loading: false } };
case GET_COMPANIES_ERROR:
return { ...state, list: { all: [], filtered: [], error: action.payload.innerException, loading: false } };
case GET_COMPANIES_PROPERTIES:
return { ...state, single: { ...state.single, object: { ...state.single.object }, error: null, loading: true } };
case GET_COMPANIES_PROPERTIES_SUCCESS:
return { ...state, single: { ...state.single, object: { ...state.single.object, payloadValues: action.payload }, error: null, loading: false } };
case GET_COMPANIES_PROPERTIES_ERROR:
return { ...state, single: { object: null, error: action.payload.innerException, loading: false } };
default:
return state;
}
}
The way I now use the spread operator in order to overwrite the old state feels dirty. I was wondering if there are any rules or guidelines to handle this issue. So far I have searched a while on internet and in specific the Redux website but I did not come cross any other solutions.
The breakage is likely due to the structure of the reducer. It is concerned with too many different parts of state and has to operate on deep nested objects, making it easy to accidentally mutate state. The guidelines for reducer structure say that splitting reducer state into normalized slices is the best way to go.
Try splitting your one reducer into multiple smaller reducers. For example:
export const all = (initialAll = [], { type, companies }) => {
switch(type) {
case GET_COMPANIES_SUCCESS: return companies;
default: return initialAll;
}
}
export const error = (initialError = '', { type, error }) => {
switch(type) {
case GET_COMPANIES_ERROR: return error;
default: return initialError;
}
}
export const isFetching = (isFetching = false, { type }) => {
switch(type) {
case GET_COMPANIES: return true;
case GET_COMPANIES_SUCCESS: return false;
case GET_COMPANIES_ERROR: return false;
default: return isFetching;
}
}
Then, compose them into one reducer:
import { combineReducers } from 'redux';
export list = combineReducers({
all,
error,
isFetching
});
// ...
export rootReducer = combineReducers({
list,
single,
// ...
})
This way, each reducer is concerned with only one thing or set of things, and its reduction handlers can do simple operations on single-level state instead of complex operations on deep nested state.
Also, in your list substate, it looks like you are storing the same type of collection resources in both all and filtered with potential overlap. This leads to multiple sources of truth for the same data, which opens the door to data inconsistency. Instead, keep an array of filteredIds:
export const filteredIds = (initialIds = [], { type, filteredIds }) => {
switch(type) {
case SET_FILTERED_IDS: return filteredIds;
default: return initialIds;
}
}
Then, use a selector that filters all by the filteredIds to get your filtered items.
One option is to use Immutable, which would change your reducers to:
case GET_COMPANIES:
return state.setIn(['list', 'loading'], true);
// etc
See Using Immutable.JS with Redux for more information about this approach.
Another option is to use Lodash, as shown in this Issue, you can define the following function to make it similar to the immutable one:
import {clone, setWith, curry} from 'lodash/fp';
export const setIn = curry((path, value, obj) =>
setWith(clone, path, value, clone(obj)),
);
Then you can use setIn as follow:
case GET_COMPANIES:
return setIn(['list', 'loading'], true, state);
// etc
The Lodash approach is just working with plain object, so it might be easier to understand than Immutable.

How to return state of Redux reducers

For redux reducer step :
What if I only want to change one single property of initial_state. For example:
const INITIAL_STATE = {
signInInfo: {
signin: false,
name: "",
email: "",
...
},
changePassword: {
status: false,
...
}
...
};
Here I only want to set signInInfo.signin as true, currently , the only way I know is to input a complete "signInInfo" like :
case SIGNIN_USER:
return { ...state, signInInfo: action.payload.data };
action.payload.data is like:
{
signin: true,
name: "Qing",
email : ...
}
And another question is what if I want to set signInInfo.signin as false and meanwhile also need to change changePassword.status from false to true.
What should I do? Can anyone give me a hint?
Fairly simple (both questions):
case SIGNIN_USER: return {
...state,
signInInfo: {
...state.signInInfo,
signin: action.payload.signin
},
changePassword: {
...state.changePassword,
status: true
}
};

Resources