Why can't I update the state using Redux? - reactjs

I'm trying to integrate Redux in a project that works already. I've configured the Redux store using multiple slices, here's the one causing troubles:
const initialCategoriesState = [];
const categoriesSlice = createSlice({
name: "categories",
initialState: initialCategoriesState,
reducers: {
setCategories(state, action) {
state = action.payload;
},
},
});
In my component I'm using useSelector to access the state:
const categories = useSelector(state => state.categories);
And to update it I dispatch an action, accordingly with the one declared in the slice:
const fetchedCategories = await fetchData(urlCategories, CATEGORIES);
dispatch(categoriesActions.setCategories(fetchedCategories));
But once I run the code, the categories constant gets never updated. Since I wasn't sure the action was getting the data, I tried to console.log the state inside the reducer it as it follows:
reducers: {
setCategories(state, action) {
console.log("state before", state);
state = action.payload;
console.log("state after", state);
},
},
The state is indeed changing, but not the extracted state in the categories constant.
Is there something I'm missing here?

I've got the same problem a few months ago and solved it this way. But, I may be missing something too.
Please try this for your initial state:
const initialCategoriesState = { categories: [] };
And then in your reducer:
state.categories = action.payload;

I've found one of the cool things about Redux Toolkit to be it's Entity Adapters, which can safely initialize your state while providing helper functions for updating your state. They (reasonably) assume that your slice will have collections, and probably a main collection of things with the same name as the slice. createEntityAdapter() allows you to do like:
const categoriesAdapter = createEntityAdapter()
// name the below "initialState" exactly
const initialState = categoriesAdapter.getInitialState({
// this will, by default, get you an `entities{}` and `ids[]` representing your categories
// you can add any additional properties you want in state here as well
})
Then, when you are wanting to update state, in your reducer you can do like:
reducers: {
setCategories(state, action) {
categoriesReducer.setAll(state, action.payload)
// this will update both entities{} and ids[] appropriately
},
},

Related

Unable to Update State ReduxToolkit

I have a array of objects kept in my state, I want to be able to edit one of the objects in the array and update the state.
However, I cannot seem to update anything with the state except push more items into it.
I am using #reduxjs/toolkit and the createSlice() method for my reducers.
Here is my slice, it has some logic to pull the initial state array from an API.
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import { BACKEND_API } from "../../utilities/environment";
import fetchViaApi from "../../utilities/fetchViaApi";
export const getInitialDashboards = createAsyncThunk(
'dashboard/getDashboards',
async () => {
const response = await fetchViaApi('/dashboards', {
baseUrl: BACKEND_API,
method: "GET"
});
const data = await response.json();
return data;
}
)
const initialState = [];
const dashboardsSlice = createSlice({
name: 'dashboards',
initialState,
reducers: {
setDashboards: (state,action) => {
state = action.payload;
},
updateDashboard: (state,action) => {
// state.push(action.payload);
state = [...state.slice(0, 5)];
},
deleteDashboard: (state, action) => {
},
},
extraReducers: builder => {
builder.addCase(getInitialDashboards.fulfilled, (state, action) => {
action.payload.forEach(element => {
state.push(element);
});
})
}
});
export const { setDashboards, updateDashboard, editDashboard, deleteDashboard } = dashboardsSlice.actions;
export default dashboardsSlice.reducer;
The commented out state.push(action.payload) works fine, but sometimes I don't want to add new object to the array, but edit existing ones.
My thought was to slice the existing element out and add the new version back to the array. But I cannot slice the state.
I am using Redux DevTools in Chrome and watching the state not change after calling updateDashboard, there were 10 elements after getDashboards is completed.
You had the right idea, but your reducers need to be returning the new state, not assigning it.. e.g.
reducers: {
setDashboards: (state,action) => {
return action.payload;
},
updateDashboard: (state,action) => {
return [...state.slice(0, 5)];
},
deleteDashboard: (state, action) => {
return [];
},
},
The issue is that state = anything is not a valid way to update data with Immer. It's not mutating the existing state, and it's not returning a new value - it just points the local state variable to something else, so Immer has no way to know that anything changed.
If you want to replace the existing state entirely, do return newStateValue. If you want to update part of the state, then mutate a nested field or value.
See the Writing Reducers with Immer page in the RTK docs for more details.
I faced a similar problem today. Updating or assigning values to the state directly is not working. But updating the properties inside the state variable works
I would add a property named dashboards to the state and update it instead of updating the state directly in reducer
Redux toolkit is using immer under the hood. It might be helpful to take a look at immer and get an idea to mutate the state

Is mutating "copy" of initial state inside a passed reducer function in "Redux Toolkits" createSlice() a bad practice?

Context
I'm trying to create the state for a multi-level accordion menu, where the top-level items are called subjects, each subject has multiple chapters and each chapter will have multiple articles.
At any given time only a single "Subject" can be in the "selected" state. The same applies to chapters and articles, with the additional restriction being they need to be "Children" of a parent in the "selected" state.
Code
I have a deeply nested object that is to be passed as the initial state to the createSlice() method,it has the following shape,
const initialState = {
currentArticle: null,
currentChapter: null,
currentSubject: null
subjects: [
{
id:"001",
chapters: [
{
id: "001001",
articles: [
{
id: "001001001",
selected: false
},
//....... more articles
],
selected: false
},
//....... more chapters
],
selected: false
},
//....... more subjects
]
}
Following is my createSlice() method,
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = initialState.subjects.map((subject) => {
if (payload.id === subject.id) {
subject.selected = true;
state.currentSubject = subject.id
}
return subject;
});
state.subjects = newNavigationState;
},
// ...... more reducer functions
},
});
The subjects array is directly used for rendering the UI, Every time a dispatch function is called I conditionally use the initial state and then calculate the next state,(those conditions are not included in the following code snippets for simplicities sake), For now let's consider that I use the initial states "subject" array every single time I need to calculate the next state instead of using the previous state passed to the reducer.
The reason for using the initial state is to not have to manually set the selected state of nested objects to false, in case the parents selected state changes.
Problem
However when I dispatch an action that executes the "setTopic" reducer function I get the following error,
TypeError: Cannot assign to read only property 'selected' of object '#<Object>'
Attempts to solve the issue
Using the spread operator to create a new copy of initialState within the reducer.
const copyInitialState = { ...initialState }
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
Using Object.assign() to create a new Object, within the reducer
const copyInitialState = {};
Object.assign(copyInitialState, initialState);
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
create 2 copies of the initial state, before invoking createSlice and pass one copy inside the createSlice() invocation as the initial state and use the other copy within the passed reducer function.
const initialStateCopy = Object.assign(initialState);
const initializedInitialState = Object.assign(initialState);
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initializedInitialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = initialStateCopy.subjects.map((subject) => {
//............
I.E : I tried this approach with the spread operator as well.
The only solution that works(not a desirable approach )
explicitly declare a completely new constant and initialize it in the exact same way as the initialState object. In this case, this simply means I'm copying the exact same object creation code one after the other so that they are completely two different objects,
const initialState = {//.... deeply nested object}
const initialStateExplicitCopy = {//.... deeply nested object}
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = initialStateExplicitCopy.subjects.map((subject) => {
Question
I think this has to do something with Immer and how it treats the initial state Object. I see that even though I do an Object.assign() nested objects are sealed and frozen.
Does this mean I'm attempting to perform something wrong? or something which is considered bad practice? Does this in any way make the reducer impure? If so I don't see why because the initial state never changes, I'm just using the initial state all the time to calculate the next state.
Is there a better approach to handle this while using the redux toolkit?
Yeah, the problem is the attempt to mutate initialState, for a couple different reasons.
It's only safe to write code that "mutates" data if that data has actually been passed through Immer and wrapped in proxies, so that Immer can track the attempted changes. When you reference initialState, that object hasn't been handed to Immer yet, so your code really is trying to mutate initialState.
Fortunately, when you call createSlice({initialState: someInitialStateValue}), createSlice internally "freezes" that value to make sure you don't accidentally mutate it for real later on. That's why you're getting the error - it's telling you you are doing something wrong.
Conceptually, I'm not sure why you're trying to always base the calculations off of initialState. Wouldn't you want to be doing updates based on the current state as the starting point?
If you truly do need to use initialState as the starting point, the best option is to use Immer directly and feed it initialState. Immer's main function is exported from RTK as createNextState, so you can use that to wrap your current logic
import { createNextState } from "#reduxjs/toolkit";
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = createNextState(initialState.subjects, draftSubjects) => {
const subject = draftSubjects.find(subject => subject.id === payload.id);
if (subject) {
subject.selected = true;
state.currentSubject = subject.id
}
}
state.subjects = newNavigationState;
},
}
});

How to reset a redux slice state with Redux Toolkit (RTK)

I am trying to reset state via a redux action called resetState, where I assign the state to the initialState variable, however this does not work.
const initialState = {
someArray: [],
someEvents: {}
}
const stateSlice = createSlice({
name: "someState",
initialState,
reducers: {
...someActions,
resetState: (state, action) => {
// THIS DOES NOT WORK
state = initialState
},
resetState2: (state, action) => {
// THIS WORKS!!!!
return initialState
},
});
When I return the initialState, the state resets properly, but assigning it to initialState does not.
Assigning state = anything is not correct, regardless of whether you're using Redux Toolkit or writing the reducers by hand. All that does is point the local state variable to a different reference.
RTK's createSlice uses Immer inside. Immer primarily works by tracking mutations to a wrapped value, such as state.someField = 123. Immer also allows you to return an entirely new state you've constructed yourself.
So, in this case, what you want is return initialState to return a new value and force Immer to replace the old one.
resetState: (state, action) => {
Object.assign(state, action.payload)
},

I am unable to update my state in Redux with dispatched action (using Redux-starter-kit)

I am very new to Redux and React-Native. I have a state which contains an array of Expense objects. One of the attributes is comment, which I am trying to update from a modal.
I think I have my code mostly right, but for some reason, the state is not updating with the newly updated item.
Modal Component code below:
const expense = useSelector(state => state.expenses.model.find( expense => expense.id === expenseId ))
const updateExpense = (updatedExpense) => dispatch(model.actions.updateExpense(updatedExpense))
const addComment = () => {
const updatedExpense = {
...expense,
comment: "hi"
}
updateExpense (updatedExpense)
}
Just to note,index is an attribute of the expense object.
and then here is where I set up my data model store and reducers:
export const model = createSlice({
slice: "model",
initialState: [],
reducers: {
fetchSuccess: (state, { payload }) => (state = payload),
updateExpense: (state, {payload}) => (
console.log ("...State: ", state),
console.log ("Payload", payload),
state = [
...state.slice(0,payload.index),
payload,
...state.slice (payload.index)
],
/* state = {
...state, [payload.index]:{
...state[payload.index],
comment: payload.comment*/
console.log ("State: ", state)
)
}
});
My logs tell me that payload contains the correct information, its just not updating the state.
Cheers.
It looks like you're using redux-starter-kit, but you don't say you are or aren't. If you're not, ignore this answer.
Right now you're setting state in your reducer: state is a reference local to the reducer. You either need to modify a state property, or return the new state, as described in the r-s-k docs, e.g.,
updateExpense: (state, { payload }) => [
...state.slice(0, payload.index),
payload,
...state.slice(payload.index)
]
Same goes for fetchSuccess.
You should return the changed state your code it's not returning the state

Redux-Saga using Immutable.js and state is always empty

I am trying to wrap my head around redux and sagas and I think I have set something up wrong and i'm hoping someone can lend some insight.
I have created my store with my inital state and I dispatch an action, as seen here:
const initialState = fromJS({
product: {},
basket: {},
global: {}
});
const reducers = combineReducers({ product, basket, global });
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers,
initialState,
applyMiddleware(sagaMiddleware))
initSagas(sagaMiddleware);
store.dispatch(retrieveSiteKeyValues())
return store;
};
Combine Reducers is from redux-immutable.
My saga function:
export function* fetchSiteKeyValuesSaga() {
yield take(RETRIEVE_SITE_KEY_VALUES)
const siteKeyValues = yield call(retrieveSiteKeyValues)
yield put(storeSiteKeyValues(siteKeyValues));
}
My reducer function:
const storeSiteKeyValues = (state, payload) => {
payload.anotherObject = {};
payload.anotherMap = new Map();
const newState = fromJS({ payload })
return newState
// OR return state.merge({ global: { siteKey: action.siteKey } }); ?
}
When I interrogate the state object the size is zero. I expected the size to be at least 3 due to my initalState. When the newState is 'created' the size is 4. But when it drops back into the state switch statement, the state size is zero again:
export default (state, action) => {
switch (action.type) {
case STORE_SITE_KEY_VALUES : {
return storeSiteKeyValues (state, action.payload);
}
default:
return state;
}
}
Im 90% sure just dumping over the state as I am doing in the reducer function is wrong and i should be using set() or setIn() i thought update() would make more sense, but when I use those methods the state is always empty or 'undefined' if I try to enact .get(x) in the console.
When I inspect the state in the browser it looks like this:
storeState:{
[2],
[2]
[2]
}
The array expanded looks like this:
0:"product"
1:{anotherObject :{}, anotherMap: map()
size:1
I would expect the values that were part of of the payload to be here not just the new object and map.
Am I initaiting my state incorrectly at the store creation? Am I approaching redux and state management in the wrong way?
I want to be sure you aren't missing a fundamental part: where is the sagaMiddleware.run(YOUR_SAGA); call? Is it hidden inside initSagas?
It was setting my inital state twice, once when I was initialsing my store and again when the reducer inital state was read. My state in my reducer was an empty object as it would be if on the time of reducer 'activation'. In the end I realised i'm not reading some 'remembered' state from anywhere, I just needed some inital values. Which I moved into the reducer and remvoed the immutable js from my app as it was confusing matters.
Some lessons for all you newbies to react/redux-saga! Don't try and over complicate matters. Learn what immutable mean! Figure out for yourself if you need it, in my case just having one source of truth and access to state was enough.
Further reading:
Initalising State ,
Immutable and State considerations

Resources