Updating an item in Redux using ImmutableJS given a key - reactjs

Redux State:
let initialState = Immutable.fromJS({
budgetItems: [],
editingBudget: [[]]
});
Trying to update items in the budgetItems section, which is an array of objects.
The structure of the objects in the array is like:
let initBudget = {budgetCategory: '', budgetCost: '', budgetDate: ''};
My attempt:
case types.UPDATE_EXISTING_BUDGET:
return state.getIn(['budgetItems']).update(
state.getIn(['budgetItems']).findIndex(function(item) {
return item.get("budgetCategory") === action.payload.budgetCategory;
}), function(item) {
return item.set(action.payload.budgetCategory);
}
);
Trying to literally replace the entire object that I've found. The above code is working if I set a single key with a value, but not the entire object

To update an object or an array you'll need the updateIn method.
import { fromJS, List, Map } from immutable;
const initialState = fromJS({
data: List([Map({ foo: { bar: baz } } })],
});
functionFooReducer(state = initialState, action) {
switch (action.type) {
case UPDATE:
return state
.updateIn(['data.0.foo.bar'.split('.')], (), action.value);
// ...
}
}

Related

React: context hook, push object into an array

The error I'm getting is Cannot find name 'objectArray'.
interface StateInterface {
objects: {
objectArray: object[];
selected: object;
};
}
const InitialState: StateInterface = {
objects: {
objectArray: [],
selected: {},
},
};
const Reducer = (state: StateInterface, action: any) => {
switch (action.type) {
case 'SELECTED':
return {
...state,
objects: { ...state.objects, selected: action.value },
};
case 'ADD_OBJECT':
return {
...state,
objects: { ...state.objects, objectArray: objectArray.push(action.value )},
// ^---- Cannot find name 'objectArray'.ts(2304)
};
default:
return state;
}
};
I also tried
objects: { ...state.objects, objectArray: ...action.value )},
Only the state object is in scope at that point (provided as an argument to the reducer), try switching objectArray for state.objectArray at the point you're getting the error.
But also, you'll need to append the value immutably for it to be correct (a rule of reducers), so you'll need to make that whole line something like:
objects: { ...state.objects, objectArray: [...state.objectArray, action.value]},
To create a new array with both the old values and the new value you're adding.

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.

Updating state in reducer using variables

I'm build a simple app that expands and collapses sections of content based on their state. Basically, if collapse = false, add a class and if it's true, add a different class.
I'm using Next.js with Redux and running into an issue. I'd like to update the state based on an argument the action is passed. It's not updating the state and I'm not sure why or what the better alternative would be. Any clarification would be great!
// DEFAULT STATE
const defaultState = {
membership: 'none',
sectionMembership: {
id: 1,
currentName: 'Membership',
nextName: 'General',
collapse: false
},
sectionGeneral: {
id: 2,
prevName: 'Membership',
currentName: 'General',
nextName: 'Royalties',
collapse: true
}
}
// ACTION TYPES
export const actionTypes = {
SET_MEMBERSHIP: 'SET_MEMBERSHIP',
MOVE_FORWARDS: 'MOVE_FORWARDS',
MOVE_BACKWARDS: 'MOVE_BACKWARDS'
}
// ACTION
export const moveForwards = (currentSection) => dispatch => {
return dispatch({ type: actionTypes.MOVE_FORWARDS, currentSection })
}
// REDUCERS
export const reducer = (state = defaultState, action) => {
switch (action.type) {
case actionTypes.SET_MEMBERSHIP:
return Object.assign({}, state, {
membership: action.membershipType
})
case actionTypes.MOVE_FORWARDS:
const currentId = action.currentSection.id
const currentName = "section" + action.currentSection.currentName
return Object.assign({}, state, {
currentName: {
id: currentId,
collapse: true
}
})
default: return state
}
}
The currentName variable is causing an issue for the state to not update. I want to be able to dynamically change each sections state, which is why I thought I'd be able have a variable and update state like this.
It seems you can't use a variable for the key in the key/value pair. Why is this? What's an alternative to dynamically updating state?
That is because JavaScript understands that you want to create a key named currentName not a key with the value of the variable currentName. In order to do what you want, you have to wrap currentName in brackets:
return Object.assign({}, state, {
[currentName]: {
id: currentId,
collapse: true
}
})
So it will understand that the key will be whatever currentName is.
It also right:
return Object.assign({}, state, {
[currentName]: Object.assign({}, state[currentName], {
id: currentId,
collapse: true
})
})

How to replace part of the Redux state

I have a packageReducer which keeps the packageType and packageList related details. once the details are fetched from the server I need to replace the initial state values with the new values that are been fetched. As an example, if the packageLists are been fetched I need to replace only the "packageList"
Below is my PackageState reducer,
const initialState = {
packageList: packageListInitialState,
packageTypes: [{title: 'Select Package Type', value: ''}],
};
export default function packageState( state = initialState, action ) {
switch ( action.type ) {
case FETCH_PACKAGE_LIST_SUCCESS:{
return Object.assign( {}, state, action.payload );
}
case FETCH_PACKAGE_TYPES_SUCCESS:{
return Object.assign( {}, state, action.payload );
}
default:
return state;
}
}
The way I have implemented I think im replacing the entire state, Can someone let me know how I can achieve it?
Thank you.
You are not:
var state = {a: 1, b: 2, c: 3}
var newData = {a: 4, b: 5}
console.log(Object.assign( {}, state, newData )) // { a: 4, b: 5, c: 3 }
Properties in the target object will be overwritten by properties in the sources if they have the same key. Later sources' properties will similarly overwrite earlier ones. (docs)
So, as long as your payload contains the keys that you really want to update, you are safe. You can also do it in a simpler way if you use ES6's spread syntax (I'm assuming your payload looks like {packageList: data}):
const initialState = {
packageList: packageListInitialState,
packageTypes: [{title: 'Select Package Type', value: ''}],
};
export default function packageState( state = initialState, action ) {
switch ( action.type ) {
case FETCH_PACKAGE_LIST_SUCCESS:{
return {...state, ...action.payload};
}
case FETCH_PACKAGE_SETTINGS_SUCCESS:{
return {...state, ...action.payload};
}
default:
return state;
}
}
Under the assumption that the action you're trying to achieve this in is only the FETCH_PACKAGE_LIST_SUCCESS action, and the payload is the updated/fetched list, then you just need to return an object as shown below.
Since you're trying to return an object with only one of the two properties changed, then you can use the previous state's value for the unchanged property and update the other.
const initialState = {
packageList: packageListInitialState,
packageTypes: [{title: 'Select Package Type', value: ''}],
};
export default function packageState( state = initialState, action ) {
switch ( action.type ) {
case FETCH_PACKAGE_LIST_SUCCESS:{
return { packageList: action.payload, packageTypes: state.packageTypes }
}
case FETCH_PACKAGE_SETTINGS_SUCCESS:{
return Object.assign( {}, state, action.payload );
}
default:
return state;
}
}

React redux - issues adding multiple items to an array in state tree object

I am looking at redux and adding names to an array. The code below works (kind of!).
I have a few issues.
I know that it is advised to create a new state tree object each time the state is passed through the reducer, however I thought it should still work even if I change the state object passed in.
In my code below the console.log(store.getState()); works if I use var newArr = state.names.concat(action.name); but not if I use state.names.push(action.name);
If I add another store.dispatch(action) the code doesn't work.
store.dispatch({type: 'ADD_NAME',name: 'PhantomTwo'});
Can anyone explain why this is so?
Finally, do I need to return state again outside the switch statement?
Here is the code I currently have below.
const initialState = {
names: []
}
function namesApp(state = initialState, action) {
switch(action.type) {
case 'ADD_NAME':
var newArr = state.names.concat(action.name);
return newArr;
default:
return state;
}
}
let store = createStore(namesApp);
store.dispatch({
type: 'ADD_NAME',
name: 'Phantom'
});
console.log(store.getState()); //returns `["Phantom"]`
This is the behavior of array object mutability
Since React highly cares about state change for re-rendering, so we need to take care of mutability.
The below snippet explains the array mutability.
let x = [];
let y = x;
console.log(x);
console.log(y);
y.push("First");
console.log(x);
console.log(y);
let z = [...x]; //creating new reference
console.log(z);
x.push("Second");
console.log(x); //updated
console.log(y); //updated
console.log(z); //not updated
So for better functionality your reducer will be like
function namesApp(state = initialState, action) {
switch(action.type) {
case 'ADD_NAME':
return {
...state, //optional, necessary if state have other data than names
...{
names: [...state.names, action.name]
}
};
default:
return state;
}
}
[].concat returns a new array. But your state was { name: [] }. Inspite of returning newly build object with new names, the code above returned the new names array.
Vanilla solution
const initialState = { names: [] };
function namesApp(state = initialState, action) {
switch(action.type) {
case 'ADD_NAME':
var newArr = state.names.concat(action.name);
return {
...state,
names: newArr
};
default:
return state;
}
}
immutability-helper
For this type of work I would use immutability-helper
import u from 'immutability-helper';
function namesApp(state = initialState, action) {
switch(action.type) {
case 'ADD_NAME':
return u(state, {
names: {
$push: action.name
}
});
default:
return state;
}
}
learn how to use immutability-helper https://facebook.github.io/react/docs/update.html

Resources