React Redux: update local state with a new value on action dispatch - reactjs

I have an action that creates a 'supplier' in the database, and when it finishes it calls another action that is supposed to create a supplier locally (through redux).
This is very basic but I am a bit lost on how to add the new supplier to the already existing supplier state.
Here is the action (The newly created supplier is passed to it):
export const createSup = (sup) => {
return {
type: "CREATE_SUP",
sup,
};
};
Here is my reducer file along with the error line (Everything working fine except for the CREATE_SUP case):
const initialState = {
value : {},
loaded: false,
error: false
}
const supReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_SUPPLIERS':
return {
value: action.sups,
loaded: false, //this might need to be set to true
error: false
}
case 'LOAD_SUPPLIERS_ERROR':
console.log("load suppliers error")
return {
...state,
loaded: false,
error: true
}
case 'LOAD_SUPPLIERS_SUCCESS':
return {
...state,
loaded: true,
error: false
}
case 'CREATE_SUP':
return {
value: {...state.value, action.sup}, //************ERROR *************//
loaded: true,
error: false
}
default:
return state;
}
};
export default supReducer;
It gives me an error mark under action.sup (I guess it expects only one value). Should I not worry about returning the old value of the state, and just return the new supplier? (I think I am missing a point or two on how exactly reducers work).
Thank you for any help.

My guess would be that you meant to use the spread operator, like so:
case 'CREATE_SUP':
return {
value: { ...state.value, ...action.sup },
loaded: true,
error: false
}
action.sup isn't an object property, it's an object. I assume you just want to apply those properties to your state object.
I should also note that your reducer should typically always return a state object in the same "shape" for every action. So if your initial state object is { prop1: 1, prop2: 2, prop3: 3 }, it should always return something similar to that, just with different values. I say this because it looks like your "LOAD_SUPPLIERS" action is kinda doing its own thing and not copying the current state. Just remember that your state object is meant to be accessed "globally" (I use this term loosely) with Redux. The components that use your Redux state will expect a certain object shape and certain properties to always be there, so you need to make sure you always return that with your reducer. Hope that makes sense.

Related

Update deeply nested state object in redux without spread operator

I've been breaking my head for a week or something with this !!
My redux state looks similar to this
{
data: {
chunk_1: {
deep: {
message: "Hi"
}
},
chunk_2: {
something: {
something_else: {...}
}
},
... + more
},
meta: {
session: {...},
loading: true (or false)
}
}
I have an array of keys like ["path", "to", "node"] and some data which the last node of my deeply nested state object should be replaced with, in my action.payload.
Clearly I can't use spread operator as shown in the docs (coz, my keys array is dynamic and can change both in values and in length).
I already tried using Immutable.js but in vain.. Here's my code
// Importing modules ------
import {fromJS} from "immutable";
// Initializing State ---------
const InitialState = fromJS({ // Immutable.Map() doesn't work either
data: { ... },
meta: {
session: {
user: {},
},
loading: false,
error: "",
},
});
// Redux Root Reducer ------------
function StoreReducer(state = InitialState, action) {
switch (action.type) {
case START_LOADING:
return state.setIn(["meta"], (x) => {
return { ...x, loading: true };
});
case ADD_DATA: {
const keys = action.payload.keys; // This is a valid array of keys
return state.updateIn(keys, () => action.payload); // setIn doesn't work either
}
}
Error I get..
Uncaught TypeError: state.setIn (or state.updateIn) is not a function
at StoreReducer (reducers.js:33:1)
at k (<anonymous>:2235:16)
at D (<anonymous>:2251:13)
at <anonymous>:2464:20
at Object.dispatch (redux.js:288:1)
at e (<anonymous>:2494:20)
at serializableStateInvariantMiddleware.ts:172:1
at index.js:20:1
at Object.dispatch (immutableStateInvariantMiddleware.ts:258:1)
at Object.dispatch (<anonymous>:3665:80)
What I want ?
The correct way to update my redux state (deeply nested object) with a array containing the keys.
Please note that you are using an incredibly outdated style of Redux. We are not recommending hand-written switch..case reducers or the immutable library since 2019. Instead, you should be using the official Redux Toolkit with createSlice, which allows you to just write mutating logic in your case reducers (and thus also just using any helper library if you want to use one).
Please read Why Redux Toolkit is how to use Redux today.
you could use something like that:
import { merge, set } from 'lodash';
export default createReducer(initialState, {
...
[updateSettingsByPath]: (state, action) => {
const {
payload: { path, value },
} = action;
const newState = merge({}, state);
set(newState, path, value);
return newState; },
...}

ReactJS - Proper way for using immutability-helper in reducer

I have the following object which is my initial state in my reducer:
const INITIAL_STATE = {
campaign_dates: {
dt_start: '',
dt_end: '',
},
campaign_target: {
target_number: '',
gender: '',
age_level: {
age_start: '',
age_end: '',
},
interest_area: [],
geolocation: {},
},
campaign_products: {
survey: {
name: 'Survey',
id_product: 1,
quantity: 0,
price: 125.0,
surveys: {},
},
reward: {
name: 'Reward',
id_product: 2,
quantity: 0,
price: 125.0,
rewards: {},
},
},
}
And my reducer is listening for an action to add a reward to my object of rewards:
case ADD_REWARD:
return {
...state, campaign_products: {
...state.campaign_products,
reward: {
...state.campaign_products.reward,
rewards: {
...state.campaign_products.reward.rewards,
question: action.payload
}
}
}
}
So far so good (despite the fact that every object added is named "question")... its working but its quite messy. I've tried to replace the reducer above using the immutability-helper, to something like this but the newObh is being added to the root of my state
case ADD_REWARD:
const newObj = update(state.campaign_products.reward.rewards, { $merge: action.payload });
return { ...state, newObj }
return { ...state, newObj }
First, you must understand how the object shorthand works. If you're familiar with the syntax before ES2015, the above code translates to:
return Object.assign({}, state, {
newObj: newObj
});
Note how the newObj becomes a key and a value at the same time, which is probably not what you want.
I assume the mentioned immutability-helper is this library: https://www.npmjs.com/package/immutability-helper. Given the documentation, it returns a copy of the state with updated property based on the second argument.
You're using it on a deep property so that it will return a new value for that deep property. Therefore you still have to merge it in the state, so you have to keep the approach you've labelled as messy.
What you want instead is something like:
const nextState = update(state, {
$merge: {
campaign_products: {
reward: {
rewards: action.payload
}
}
}
});
return nextState;
Note how the first argument is the current state object, and $merge object is a whole object structure where you want to update the property. The return value of update is state with updated values based on the second argument, i.e. the next state.
Side note: Working with deep state structure is difficult, as you've discovered. I suggest you look into normalizing the state shape. If applicable, you can also split the reducers into sub-trees which are responsible only for the part of the state, so the state updates are smaller.

Using redux with react, why is fetching new data deletes part of my previous state?

I have multiple requests happening one after the other, I handle them with actions and reducers but some of them seem to delete elements of my state when completed.
Can someone explain to me why this is the case?
Here is my reducer:
...
case FETCH_BLOG:
return { ...state, blogs: action.payload.data };
case FETCH_ITEM_LIST:
return { ...state.item, done: true, popular: [ ...state.popular ], nearby: [ ...state.nearby ], item: { ...state.item }, second_item: { ...state.second_item }, items: action.payload.data.item , new_item: action.payload.data.new_item, item_places: action.payload.data.item_places, stories: action.payload.data.stories };
case FETCH_ITEM_NEARBY:
return { ...state, nearby: action.payload.data.nearby, loading: false, count: action.payload.data.count, done: true };
case FETCH_ITEM_NEARBY_START:
return { ...state, loading: true };
case FETCH_ITEM_POPULAR:
return { ...state, popular: action.payload.data.popular };
...
I thought using ...state would keep the previous state and just add elements to it but it seems like it overrides it somehow.
I call my actions in this order, and I can see that after some actions are finished, part of my previous state is deleted.
this.props.itemNearbyFetch();
this.props.itemPopularFetch();
this.props.itemListFetch();
this.props.blogFetch();
in your FETCH_ITEM_LIST action you are only spreading the item into state, which would get rid of any attributes of the state object and replace with whatever attributes your item has. I think you are misunderstanding how the spread operator works.
What you want to do is something like
case FETCH_ITEM_LIST:
return { ...state,
done: true,
items: action.payload.data.items,
new_item: action.payload.data.new_item,
item_places: action.payload.data.item_places,
stories: action.payload.data.stories
};
In the previous example you would now have anything that was previously on state like popular and nearby be copied, stories, items, and item_places from the payload data are returned to the new state.
doing item:{...state.item} is redundant as it is handled when you do {...state, ...}

Create reducer to update state for different components in redux

I am creating this UI using react and redux. Below is the image of the UI.
This is image of UI
Now it currently displays the data when 'Categories' is clicked. Then when I click on unmapped link ,it shows unmapped products of 'Categories'. But when I click on 'Addons' and then on unmapped link ,it still shows the unmapped products of 'Categories'. How do I write the reducer so that it shows the unmapped products of 'Addons' as well. In my react main file I use (this.props.menu_items) that's why it shows the data of 'Categories' when clicked unmapped after Addons.
This is my reducer file.
import { AppConstants } from '../constants';
const initialState = {
state: [],
menu_items: [],
menu_items_copy: [],
addon_items: [],
unmapped: false,
};
export default (state = initialState, action) => {
switch (action.type) {
case AppConstants.getMenuItems:
return {
...state,
}
case AppConstants.getMenuItemsSuccess:
return {
...state,
menu_items: action.menu_items,//This contains the data showed when
// 'categories' is clicked
menu_items_copy: action.menu_items,
unmapped: false,
}
case AppConstants.getAddonsItems:
return {
...state,
}
case AppConstants.getAddonsItemsSuccess:
return {
...state,
menu_items: action.addon_items,//Here I update the data to addons data
}
case AppConstants.showAllProducts:
return {
...state,
menu_items: action.menu_items,
unmapped: false
}
case AppConstants.getUnmappedMenuItemsSuccess:
return {
...state,
unmapped: true,
menu_items: state.menu_items_copy.filter((item) => {-->How do I write
here for action.addon_items also
return (item.productList.length == 0)
})
}
case AppConstants.hideError:
return {
...state,
error: null
}
default:
return state
}
};
The only state I would change via reducer when unmapped is clicked would be to set unmapped: true / false.
There is no need to filter the items array and store it back into the state, as you already have all the info you need to derive your combined items at the point where you pass it to the view.
Have a read about derived state and selectors here http://redux.js.org/docs/recipes/ComputingDerivedData.html
In order to do this you would use a selector to combine the relevant parts of your state to produce a single list of items that is derived from both the items arrays, depending on the unmapped flag. The reselect library is a great helper to do this while memoising for performance, it will only recompute if any of the inputs change, otherwise returns the previously cached value
import {createSelector} from 'reselect'
const selectMenuItems = state => state.menu_items
const selectAddonItems = state => state.addon_items
const selectUnmapped = state => state.unmapped
const selectItemsToShow = createSelector(
selectMenuItems,
selectAddonItems,
selectUnmapped,
(menuItems, addonItems, unmapped) => {
// if unmapped is set - combine all items and filter the unmapped ones
if (unmapped) {
return menuItems.concat(addonItems).filter(item => !item.productList.length)
}
// else just return the menu items unfiltered
return menuItems
}
)
// this selector can then be used when passing data to your view as prop elsewhere
// or can be used in `connect` props mapping as in the linked example)
<ItemsList items={selectItemsToShow(state)} />

reducer: adding to array data

if i pull some data from an external source fro the initial state, then want to add additional information like for example 'liked'?
i've tried adding to the products array but its go messy, I'm thinking i should have an additional array for liked items then put the product id in this, the only thing is i need it to reflect in the product that it has been liked and I'm mapping the product data to the item.
whats the best way to go about this ?
const initialState = {
isFetching: false,
products: [],
};
should i add favs: [] ?
how would i reflect the liked state to my product as I'm mapping the products array to the product component? and the liked state is now in the favs?
i tried doing this to add it to the product array but it got really messy (something like this)
case ADD_LIKED:
state.products[action.index]['liked'] = true;
return state;
state.products[action.index]['liked'] = true;
The problem here is that you are mutating the state inside the reducer which is one of the things you should never do inside a reducer.
You'll find that writing functions which don't mutate the data are much easier if you break them down into smaller parts. For instance you can start to split your application up.
function productsReducer(products = [], action) {
// this reducer only deals with the products part of the state.
switch(action) {
case ADD_LIKED:
// deal with the action
default:
return products;
}
}
function app(state = {}, action) {
return {
isFetching: state.isFetching,
products: productsReducer(state.products, action)
}
}
In this case I would definitely want to write a little immutability helper.
function replaceAtIndex(list, index, replacer) {
const replacement = replacer(list[index]);
const itemsBefore = list.slice(0, index),
itemsAfter = list.slice(index + 1);
return [...itemsBefore, replacement, ...itemsAfter];
}
You can complement this with a generic function for changing objects in lists.
function updateInList(list, index, props) {
return replaceAtIndex(list, index, item => {
return { ...props, ...item };
});
}
Then you can rewrite your function in the immutable form
switch(action) {
case ADD_LIKED:
return updateInList(products, action.index, { liked: true });
default:
return products;
}
You could even get fancy by partially applying the function. This allows you to write very expressive code inside your reducers.
const updateProduct = updateInList.bind(this, products, action.index);
switch(action) {
case ADD_LIKED:
return updateProduct({ liked: true });
case REMOVE_LIKED:
return updateProduct({ liked: false });
default:
return products;
}

Resources