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
Related
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;
},
}
});
I am following the tutorial on the official website.
I know that the state is immutable and the operation in reducer is actually create a new copy of the state, modify it and replace the original one.
I am thinking:
isn't it suppose to delete the entry(exisitngPost) in the array and push a new one in instead? As state.find() should return by value instead of reference. Do I get it wrong? or it is something to do with reducer logic?
tutorial link
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded(state, action) {
state.push(action.payload)
},
postUpdated(state, action) {
const { id, title, content } = action.payload
// where i find confused<<<<<<<<<<<<<<<<<<<
const existingPost = state.find(post => post.id === id)
if (existingPost) {
existingPost.title = title
existingPost.content = content
}
}
}
})
export const { postAdded, postUpdated } = postsSlice.actions
export default postsSlice.reducer
state.find returns the object in state there - and if it is not a primitive, that object will be a reference. So at that point in a RTK immer reducer you can modify it and after the reducer is run, a new immutable copy with the applied changes will be created for you instead of modifying the original.
I try to render items that are in my firestore database. It displays correctly on the first render but when I switch between tabs/navigations it will readd the data to the list again. If I keep on switching between the tabs it will add more and more.
When I use local state there is no issue with that but the issue is with when I use dispatch to display items it will readd more items to the list(not the firestore database).
useEffect(
() =>
onSnapshot(collection(db, "users"), (snapshot) => {
console.log(
"snapshot",
snapshot.docs.map((doc) => doc.data())
);
const firebaseData = snapshot.docs.map((doc) => doc.data());
dispatch(addItem(firebaseData));
setLocalState(firebaseData);
}),
[]
);
itemsSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
items: [],
};
const itemsSlice = createSlice({
name: "addItems",
initialState,
reducers: {
addItem: (state, action) => {
state.items.push(...action.payload);
},
emptyItems: (state) => {
state.items = [];
},
},
});
export const { addItem, emptyItems } = itemsSlice.actions;
export default itemsSlice.reducer;
Here is a gif of the issue:
https://gyazo.com/bf033f2362d9d38e9eb315845971e224
Since I don't understand how you're switching between tabs, I can't tell you why the component seems to be rerendering. The useEffect hook is responsible to act like both the componentDidMount and componentDidUpdate methods. And the global redux state is not going to reset itself after each render (which is why it's so great).
This is not the case for a component's state which well reset each time a component is removed from and added to the dom. This explains why when you use a local state, the problem disappears. It's not that its fixed, it's because the state is reset every time the component is removed and added (which is what seems to be happening here).
Since we don't have the full details a quick fix should be to replace this
state.items.push(...action.payload);
with this
state.items = [...action.payload];
this will set a new value to state.items instead of pushing to it.
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
},
},
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