Redux reducers adding a value to complex multidimensional object - reactjs

i have an object in my initialState in redux reducer.The object is multidimesional.the object is like below
tradings: {
'buy': {
data: []
},
'sell': {
data: []
},
'total': {
data: []
}
},
So when i want to reduce an action payload (i.e: '1212') how i can push item into data array?
for example:
case 'EXCHANGE_BUY' : {
return{
...state,
tradings: state.tradings['buy'].data.concat(action.payload)
}
}
But it only returns an array like this: tradings: ['1212'] How can i get like this>
tradings: {
'buy': {
data: ['1212']
},
'sell': {
data: []
},
'total': {
data: []
}
},

You need to make sure you clone every level of your redux reducers state. You can do it like this:
case 'EXCHANGE_BUY' : {
return{
...state,
tradings: {
...state.tradings,
buy: {
data: state.tradings.buy.data.concat(action.payload)
}
}
}
}

case 'EXCHANGE_BUY':
return {
...state,
tradings: {
...state.tradings,
buy: {
data: {
state.tradings.buy.data.concat(action.payload)
}
}
}
}
Or the second approach you can use is Immutability helpers.
Thanks.

Use the spread operator to clone until the level is reached:
case 'EXCHANGE_BUY' : {
return{
...state,
tradings: {
...state.tradings,
buy : {
data: [...state.tradings.buy.data, action.payload]
}
}
}
}

Related

Add to list of an object with variable key in Redux

Intended for this reduce function to add new message into a list of messages with a variable key userId.
let state = {
data: {
user1: ["message1", "message2"],
user2: ["message3", "message4"],
},
};
This is what I've got so far:
case types.MESSAGE_FETCHED:
return {
...state,
data: {
...state.data,
[payload.userId]: [...state.data[payload.userId], payload.message],
},
};
The error I'm getting this error:
TypeError: state.data[payload.userId] is not iterable
I found that, I have to initialize an empty array first.
case types.MESSAGE_FETCHED:
if (!(payload.userId in state.data)) {
state.data[payload.userId] = [];
}
return {
...state,
data: {
...state.data,
[payload.userId]: [...state.data[payload.userId], payload.message],
}
}

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.

Composing a redux reducer for nested objects in an array

I'm new to Redux and am having some difficulty composing a working reducer for my situation.
My current state looks like this
export const userData = {
userID: '12345678',
userDetails: {
firstName: 'Joe',
surname: 'Bloggs'
},
currentGames: [
{
gameId: 'G-00000001',
gameSelections: [
{
subgameId: '',
selection: ''
}
]
}
]
};
My action looks like this
function selectWinner (gameId, subgameId, selection) {
return {
type: SELECT_WINNER,
gameId,
subgameId,
selection
}
}
The aim is to be able to add/update the objects in the gameSelections array.
There may be more than one Object in the currentGames array also.
I've heard I should use .map but I'm not really sure how.
You're on the right track for using .map() to iterate over the array of objects. It also looks like your action-creator has all the necessary parameters to update your reducer state.
Your reducer can look something like this:
const userReducer = (state=userData, action) => {
switch(action.type){
case SELECT_WINNER:
return {
...state,
currentGames: [...state.currentGames].map((game) => {
if(game.gameId == action.gameId){
return {
...game,
gameSelections: [...game.gameSelections].map((gameSelection) => {
if(gameSelection.subgameId == action.subgameId){
return {
...gameSelection,
selection: action.selection
}
} else {
return gameSelection
}
})
}
} else {
return game
}
})
}
default:
return state
}
}
Kind of messy, but would get the job-done with a deeply nested state.
Add item to array:
case'ADD_ITEM':
return {
...state,
some_arr: [...state.some_arr, action.payload]
}
update spicific item in array:
case 'UPDATE_ITEM':
return {
...state,
some_arr: state. some_arr.map(
(item, index) => index === specific_index
? {...item, ...action.payload}
: content
)
}
Deep cloning of the state is required.
useful link-https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns
const reducer = (state = userData, action) => {
switch (action.type) {
case CASENAME:
return {
userID: state.userID,
userDetails: {
...state.userdetails
},
currentGames: [
{
gameId: action.gameId,
gameSelections: [
{
subgameId: action.subgameId,
selection: action.selection
}
]
}
]
};
}
}

I want to use spread operator to add data to my array

I want to use spread operator to add data to my array. This works when its an object
{
...state,
selections: {
...state.selections,
[action.data.type]: action.data
}
But when its an array
{
...state,
selections: {
...state.selections,
[action.data.type]: [] // Every time i save something it should end up here
}
i was thinking of
{
...state,
selections: {
...state.selections,
[action.data.type]: [
...state.selections[action.data.type],
action.data]
}
But it doesnt work because ...state.selections[action.data.type] its non-iterable instance.
This is what i want to accomplish
state: {
selections {
products: ["a", "b"]
}
}
I have tried to look into https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns
Your usage of the spread operator should be fine. I am quite sure the problem is, that you try to add action.type which in the case of your small example would be state.selections with non-iterable type object.
// example
const x = { a: 1, b: { c: []}}
const y = {...x, b: { [x.b.c] : [...x.b.c, 1] }}
console.log(y);
Try with this:
{
...state,
selections: {
...state.selections,
[action.data.type]: Array.isArray(state.selections[action.data.type])
&& [
...state.selections[action.data.type],
action.data]
|| [
state.selections[action.data.type],
action.data]
}
If state.selections[action.data.type] have initial value of [] when its an array this should work.
It would be much easier if you show example of the payload and the initialState.
Is action.data always an object ?
{
...state,
selections: {
...state.selections,
[action.data.type]: Array.isArray(state.selections[action.data.type])
? [...state.selections[action.data.type], action.data]
: action.data
}

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.

Resources