Adding item 2 levels deep in Redux Reducer - reactjs

I am trying to add notes to a task object but what I have so far adds it to all the tasks. When I try different ways, it doesn't compile. The Object.assign doesn't like coming after the .push()
When it adds to all task:
let taskReducer = function(tasks = [], action) {
switch (action.type) {
case 'ADD_NOTE':
return tasks.map((task) => {
const { notes } = task;
const { text } = action;
notes.push({
text,
id: notes.length,
})
return task.id === action.id ?
Object.assign({}, { task, notes }) : task
})
When it doesn't compile:
let taskReducer = function(tasks = [], action) {
switch (action.type) {
case 'ADD_NOTE':
return tasks.map((task) => {
return task.id === action.id ?
const { notes } = task;
const { text } = action;
notes.push({
text,
id: notes.length,
})
Object.assign({}, { task, notes }) : task
})

You almost never want to use Array.push() in a reducer, because that directly mutates the existing array, and direct mutations generally break UI updates (see the Redux FAQ). You could use push() on a new copy of the old array, but most examples don't use that approach. Most of the time, the suggested approach is to use const newArray = oldArray.concat(newValue), which returns a new array reference containing all the old items plus the new item.
Beyond that, keep in mind that when updating nested data immutably, every level of nesting needs to have a copy made and returned.
Haven't actually tested this, but I think your code needs to look roughly like this example:
let taskReducer = function(tasks = [], action) {
switch (action.type) {
case 'ADD_NOTE':
return tasks.map((task) => {
if(action.id !== task.id) {
return task;
}
const { notes } = task;
const { text } = action;
const newNotes = notes.concat({id : notes.length, text});
const newTask = Object.assign({}, task, {notes : newNotes});
return newTask;
}
default : return tasks;
}
}

Related

My reducer keeps on overwriting the value

I have a reducer that concats a base price when you click on a menu item, then after when the user clicks on a modifier for that menu item its supposed to add a modifier object to the state but instead it just returns the mod_price object only. I am looking to have it return both the base_price and the mod_price, but instead it returns the mod_price object. Below is my code. Any help would be really appreciated.
/*This shows the cart price for only the selected item next to where it says add to cart*/
export const cartPriceForSelectedItem = (state = [],action) => {
const {payload,type} = action;
switch (type) {
case SELECTED_PRICE :
const {base_price} = payload;
return state.concat({base_price: base_price});
case SET_MOD_PRICE_TOTAL :
return state.concat({mod_price: payload})
}
return state;
}
If state is an object, you are looking for spread operator or Object.assign to return the new state with the merged properties.
return {
...state,
base_price: base_price
};
Object.assign
return Object.assign({}, state, {
base_price: base_price
});
One thing to note is that these operations perform a shallow merge, which I presume is not an issue in this context.
const initialState = [];
const SELECTED_PRICE = 'SELECTED_PRICE';
const SET_MOD_PRICE_TOTAL = 'SET_MOD_PRICE_TOTAL';
const cartPriceForSelectedItem = (state = [], action) => {
const {
payload,
type
} = action;
switch (type) {
case SELECTED_PRICE:
const {
base_price
} = payload;
return state.concat({
base_price: base_price
});
case SET_MOD_PRICE_TOTAL:
return state.concat({
mod_price: payload
})
}
return state;
}
const action1 = {
type: SELECTED_PRICE,
payload: {
"base_price": 20
}
};
const state1 = cartPriceForSelectedItem(initialState, action1);
console.log('state1', state1);
const action2 = {
type: SET_MOD_PRICE_TOTAL,
payload: 45
};
const state2 = cartPriceForSelectedItem(state1, action2);
console.log('state2', state2);

how to save array object data in redux store

i try to store multiple object in redux store on my react native app, but only one object is save,
i'm new at redux, i try a lot of solutions found on StackOverflow but no one works :/
result i have in my store:
"hives": {"hive_id": 12944, "hive_name": null}
result i want (or something like that) :
"hives": [
1: {"hive_id": 123, "hive_name": "HelloHive"},
2: {"hive_id": 12944, "hive_name": null}]
store:
const middleware = [thunk]
export const store = createStore(persistedReducer, applyMiddleware(...middleware));
export const persistor = persistStore(store);
reducer :
const INIT_STATE = {
hives: [],
}
const hiveReducer = (state = INIT_STATE, action) => {
switch (action.type) {
case SET_HIVES:
return {
...state,
hives: action.payload,
};
[...]
action creator:
export const setHives = hives => {
return {
type: SET_HIVES,
payload: hives,
};
};
action:
export const getHives = () => {
return dispatch => {
axios.get(GET_HIVE_URL, HEADER).then(res => {
const status = res.data.status;
const hives = res.data.hives;
if (status == 'hiveFound') {
for (let i = 0; i < hives.length; i++) {
console.log(hives[i]);
dispatch(setHives(hives[i]));
}
}
});
};
};
and my API send me:
"hives": [
{
"hive_id": 123,
"hive_name": "HelloHive"
},
{
"hive_id": 12944,
"hive_name": null
}
]
and console.log(hives[i]) return :
LOG {"hive_id": 123, "hive_name": "HelloHive"}
LOG {"hive_id": 12944, "hive_name": null}
thanks you
First of all, in your reducer you don't need to use ...state spread operator, since hives seems to be the only one variable in your state there. And second, you are iterating over each element of hives, therefore you are inputting them one by one thus overwriting the previous one. You are not appending it to array. Here's how you need to change your action:
export const getHives = () => {
return dispatch => {
axios.get(GET_HIVE_URL, HEADER).then(res => {
const status = res.data.status;
const hives = res.data.hives;
if (status == 'hiveFound') {
dispatch(setHives(hives));
}
});
};
};
This way it will write the whole array into that variable in redux.
You can try this below so you can store the whole array. assuming you already have the actions.
InitialState
export default {
hives:[]
}
HivesReducer
export default function counter(state = initialState.hives, action) {
switch (action.type) {
case Types.SET_HIVES:
return [...state, action.payload];
default:
return state;
}
}
In your reducer try this :
case SET_HIVES:
return {
...state,
hives: [...state.hives,action.payload],
};
[...]
hope it helps. feel free for doubts

Redux adds duplicate array item when loaded from state

Hey everyone probably a simple question, basically I have a button when i click it fires an action and passes down the whole object that I concat to array if its not duplicate but strangely what happens because I save data to local-storage and after I load it from there it does not check for duplicate and duplicates the array item. My reducer code below maybe the error is there?
Searched as much as possible.
const initialState = {
favourites: []
};
const favourites = (state = initialState, action) => {
const { payload } = action;
switch (action.type) {
case actionTypes.ADD_FAVOURITES:
return {
...state,
favourites:
state.favourites.indexOf(payload) === -1
? state.favourites.concat(payload)
: state.favourites
};
default:
return state;
}
};
The issue here seems to be that state.favourites.indexOf(payload) === -1 is always true. This is because the function Array.prototype.findIndex() does not find identical objects.
You should use an alternate method of checking to see if the payload object is already in the favourites array. For example, you could try something like this:
const favourites = (state = initialState, action) => {
const { payload } = action;
switch (action.type) {
case actionTypes.ADD_FAVOURITES:
return {
...state,
favourites:
JSON.stringify(state.favourites).indexOf(JSON.stringify(payload)) === -1
? state.favourites.concat(payload)
: state.favourites
};
default:
return state;
}
};

Redux store doesn't update all changes in app.js

such an honor to drop my first question in this community! I'm working on a recipe app where I use Redux to manage states. I'm using async storage to store changes locally. I'm a bit stuck now because my store only applies and stores a few changes instead of the whole recipe.
This is how the data of a recipe looks like (sorry for my Dutch):
{
cardId: 2,
time: "5 minutes",
title: "Wortel-Kokossoep met Dadelroom",
category: "ontbijt",
image: require("./assets/wortel-kokossoep.jpg"),
subtitle: "Gezonde en makkelijke soep!",
caption: "Wortel-Kokossoep met Dadelroom",
description:
"Begin de dag gezond met deze smoothie dat rijk is aan vitamines.",
stepOne: "Stap 1: Voeg alles toe aan de NutriBullet of blender.",
stepTwo:
"Stap 2: Blend twee keer gedurende ongeveer 5 tot 10 seconden en je bent klaar!",
stepThree: "",
stepFour: "",
stepFive: "",
stepSix: "",
stepSeven: "",
stepEight: "",
favorite: false
},
and this is how I implemented Redux in the app.js. Please forgive me for posting the whole code. I'm still a noob, eager to learn everything about Redux and react.
const reducer = (state = initialState, action) => {
switch (action.type) {
case "FAV_RECIPIE":
//const recipie = state.recipies.find(r => (r.cardId = action.id));
const recipieIndex = state.recipies.findIndex(
r => r.cardId === action.id
);
const currentValue = state.recipies[recipieIndex].favorite;
state.recipies[recipieIndex].favorite = !currentValue;
state.recipies = [...state.recipies];
saveRecipes(state.recipies); // save to local storage
return { ...state };
case "SET_LOADED_RECIPIES":
console.warn("!!!!");
if (action.recipies) {
state.recipies = [...JSON.parse(action.recipies)]; // JSON parse to convert string back to list
}
console.log("set recipies");
return { ...state };
case "OPEN_MENU":
return { action: "openMenu" };
case "CLOSE_MENU":
return { action: "closeMenu" };
default:
return state;
}
};
const saveRecipes = async recipies => {
try {
await AsyncStorage.setItem("#VV:Recipes", JSON.stringify(recipies)); // JSON stringify to convert list to string (for storage)
} catch (error) {
// error saving, and that is fine =)
console.log("could not save recipes");
}
};
const store = createStore(reducer, initialState);
store.subscribe(() => {
console.log("store changed", store.getState().recipies);
});
const App = () => (
<Provider store={store}>
<AppNavigator />
</Provider>
);
export default App;
I really hope some of you can help me out! Thanks in advance!
There's a couple of things going wrong in your reducer, but the big thing is doing state-mutations. You want to avoid logic like:
state.recipies[recipieIndex].favorite = !currentValue;
also
state.recipies = [...state.recipies];
This is against redux principles. You never want to directly change values of the state without first making a copy or clone.
So we will go with creating a shallow-copy of state in your reducer and make updates to that instead:
const reducer = (state = initialState, action) => {
switch (action.type) {
case "FAV_RECIPIE":
var newState = {...state}
//const recipie = state.recipies.find(r => (r.cardId = action.id));
const recipieIndex = state.recipies.findIndex(
r => r.cardId === action.id
);
const currentValue = state.recipies[recipieIndex].favorite;
newState.recipies[recipieIndex].favorite = !currentValue;
saveRecipes(newState.recipies); // save to local storage
return { ...newState };
case "SET_LOADED_RECIPIES":
console.warn("!!!!");
var newState = [...state]
if (action.recipies) {
newState.recipies = [...JSON.parse(action.recipies)]; // JSON parse to convert string back to list
}
console.log("set recipies");
return { ...newState };
case "OPEN_MENU":
return { action: "openMenu" };
case "CLOSE_MENU":
return { action: "closeMenu" };
default:
return state;
}
};
Alternatively we can handle this succinctly using .map() which creates a copy for us.
const reducer = (state = initialState, action) => {
switch (action.type) {
case "FAV_RECIPIE":
const updatedRecipes = {
...state,
recipes: state.recipes.map(recipe => {
if (recipe.cardId === action.id) {
return {
...recipe,
favorite: !recipe.favorite
};
} else {
return recipe;
}
})
};
saveRecipes(updatedRecipes)
return {
...updatedRecipes
}
case "SET_LOADED_RECIPIES":
var newState = {...state};
if (action.recipies) {
newState.recipies = [...JSON.parse(action.recipies)]; // JSON parse to convert string back to list
}
return { ...newState };
case "OPEN_MENU":
return { action: "openMenu" };
case "CLOSE_MENU":
return { action: "closeMenu" };
default:
return state;
}
};

Unable to convert deep cloning to immer

I'm working on some legacy code that used to do full deep cloning of the redux state in a react application. Here's the original boilerplate, which is used as a basis for the reducers used in the application:
export default (initialState, handlers = {}, promiseActionTypes = []) =>
(state = initialState, action) => {
let nextState = _.cloneDeep(state)
promiseActionTypes.forEach((actionType) => {
if (action.type === `##REMOTE/${actionType}_REQUEST`) {
nextState.isFetching = true
}
if (action.type === `##REMOTE/${actionType}_SUCCESS`) {
nextState = {
...nextState,
data: _.get(action, 'data.storeData', action.data),
isFetching: false,
isInited: true,
}
}
if (action.type === `##REMOTE/${actionType}_FAILURE`) {
nextState.isFetching = false
}
})
if (handlers.hasOwnProperty(action.type)) {
nextState = handlers[action.type](nextState, action, _.cloneDeep(state))
}
return nextState
}
Those clone deeps are all big no-nos, so I'm trying to leverage immer's produce function to mutate drafted copies of the state before returning the new state.
Problem is, I've been unable to get everything in sync. Some pieces of state won't update correctly here or there. Here's my attempt at refactor so far:
import produce from 'immer'
export default (initialState, handlers = {}, promiseActionTypes = []) =>
(state = initialState, action) => {
return produce(state, (draft) => {
promiseActionTypes.forEach((actionType) => {
if (action.type === `##REMOTE/${actionType}_REQUEST`) {
draft.isFetching = true
}
if (action.type === `##REMOTE/${actionType}_SUCCESS`) {
draft.data = _.get(action, 'data.storeData', action.data)
draft.isFetching = false
draft.isInited = true
}
if (action.type === `##REMOTE/${actionType}_FAILURE`) {
draft.isFetching = false
}
return draft
})
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](draft, action, state)
}
return draft
})
}
I tried to unfreeze the touched objects, but still no dice. Is my implementation just off? Or am I misunderstanding how produce works?
Hell, do I ever need something like immer if I'm just trying to get those two lodash cloneDeep calls out of there?
EDIT: Here's an example of a custom handler that would invoke the final line:
LOCATION_CHANGE: (state, action) => {
// bootstrap
if (_.isUndefined(state.location)) {
state.location = action.location
}
state.next = {
location: action.location,
changed: action.changed,
}
state.isNavigating = true
return state
},
VIEW_ROUTE_MATCH: (state, action) => {
state.next = {
...state.next,
match: action.match,
view: action.view,
}
return state
},
I'd say you don't need Immer, in this instance, as you're not really making deep or complex changes, but it can still help tidy up parts. Immer's going to force three lines of code, so I stick with the spread operator if that'll keep it to one line.
export default (initialState, handlers = {}, promiseActionTypes = []) =>
(state = initialState, action) => {
let nextState = state;
promiseActionTypes.forEach((actionType) => {
if (action.type === `##REMOTE/${actionType}_REQUEST`) {
nextState = { ...nextState, isFetching: true };
}
if (action.type === `##REMOTE/${actionType}_SUCCESS`) {
nextState = produce(state, draft => {
draft.data = _.get(action, 'data.storeData', action.data);
draft.isFetching = false;
draft.isInited = true;
});
}
if (action.type === `##REMOTE/${actionType}_FAILURE`) {
nextState = { ...nextState, isFetching: false };
}
})
if (handlers.hasOwnProperty(action.type)) {
nextState = handlers[action.type](nextState, action);
}
return nextState
}
const handlers = {
LOCATION_CHANGE: (state, action) => {
return produce(state, draft => {
if (_.isUndefined(state.location)) {
draft.location = action.location;
}
draft.next = {
location: action.location,
changed: action.changed,
}
draft.isNavigating = true
});
},
VIEW_ROUTE_MATCH: (state, action) => {
return produce(state, draft => {
draft.match = action.match;
draft.view = action.view;
});
},
}
The first issue is that your original code really shouldn't have been doing a "deep clone" at all, and especially not at the top of the reducer. That means it was doing extra work for every dispatched action, even if it wasn't relevant, and also likely causing the UI to re-render even if the data's values hadn't really changed. So, as you said, a "big no-no". Based on that, yes, I'd say Immer is useful here.
Second, you shouldn't be using promiseActionTypes.map() conceptually, since you're trying to do some updates instead of returning a new array. Use .forEach() as the original code did.
Third, you haven't described which fields aren't being update properly, so it's a bit hard
Beyond that, the draft code looks okay. However, the handlers line looks suspicious in this case. My guess is that those additional handlers are probably doing their own immutable work, rather than attempting to "modify" the draft state value. Oh, and you aren't even passing the draft to them anyway. So, if you're expecting those to be generating the rest of the changes, you need to A) pass draft in to the handlers, and B) change them to "modify" the draft.

Resources