readding items to the list while switching between navigations(firebase) - reactjs

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.

Related

redux useSelector is not giving root state

I'm working on a project that use Redux toolkit in typescript language.
The problem is that when I use createSelector it returns wrong state object in some of the selectors, not all of them. And it will work, if I use store.getState() instead of returned state. Let me describe more details.
My state includes one reducer which includes two reducers that combined using combineReducers, as shows below.
const entities = combineReducers({
trello: entityTrello,
users: entityUser,
})
export function makeStore() {
return configureStore({
reducer: {
entities: entities,
},
devTools: process.env.NEXT_PUBLIC_PROJECT_STATUS == "develop",
})
}
trello reducer contains boards and their cards.
users reducer contains list of users.
all things go right and after saving data in store I got below state tree
Then I try to get data from state using createSelector.
export const selectLanesByBoardId = () => {
return useAppSelector<BoardData>(selector => createSelector(
(state: AppState) => state.entities.trello.tables.data?.boards,
(state: AppState) => state.entities.trello.tables.data?.lanes,
(state: AppState) => state.entities.trello.tables.data?.cards,
(state: AppState) => state.entities.trello.meta.boardId,
(state: AppState) => state.entities.trello.meta.workspace,
(boards, lists, cards, boardId, workspace) => {
let data: Array<Lane>
// because we have key-value array, first we change it to simple array using "objectValues"
// then find lanes of current board by "boardId" which sent from component (it's ids of lanes, not whole object)
const laneIds = objectValues<TrelloBoard>(boards)?.find(value => value.id == boardId)?.lists
// now with using "laneIds" search for specific lane objects
const lanes = objectValues<TrelloLanes>(lists)?.filter(value => laneIds?.includes(value.id))
// any lane has it's own cards that not implemented yet, so update cards field using "map" on array of lanes
data = lanes.map(lane => ({
...lane,
cards: objectValues<TrelloCard>(cards)
?.filter(value => lane?.cards?.includes(value.id))
.map(value => ({
...value,
draggable: workspace == "free",
laneId: lane.id
}))
}))
return {lanes: data} as BoardData
}
)(selector))
}
It goes right again, and I got correct data.
But in another createSelector the returned state is not the root state. please check below code.
export const selectUsers = () => {
return useAppSelector<Array<User>>(selector => createSelector(
(state: AppState) => {
return state.entities.users.tables.data?.users
},
(users) => {
return users ?? []
}
)(selector))
}
In this part of code I get an error which shows below.
As I said, it will work if I use store.getState() instead of state. Here is the code
export const selectUsers = () => {
return useAppSelector<Array<User>>(selector => createSelector(
(state: AppState) => {
return store.getState().entities.users.tables.data?.users
},
(users) => {
return users ?? []
}
)(selector))
}
Last, my question is why createSelector is not returning the root state in some part of the code, but store.getState() does. Any help, please?
update
As the shared image of the error shows, it says that entities object is undefined, not users. So I figured out that the root state is changing as #GabrielePetrioli noted in a comment, by adding a new console.log in createSelector of users, the state shows below data in the console log.
it's showing a new object instead of my store structure.
But how may the root state entity changes? I even has no access to entities object in my reducers.
below code is one of updating samples of the state.
state.tables.data && (state.tables.data = {
...state.tables.data,
lanes: state.utils.nextStateAfterLastAction?.lanes ?? [],
cards: state.utils.nextStateAfterLastAction?.cards ?? [],
})
as you can see, I just have access to the tables object, not the entities object.
every reducer updates its own state, not the root structure of the state!

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

Redux state rerender with useSelector - how to achieve it?

I have a problem with redux re-render. Or rather lack of it, because using useState, everything works perfectly. My question is - how should I use the selector, so once the data is fetched, the component is re-rendered. Also, with redux approach sometimes the object X returns an error of undefined - because the assigned data is not yet fetched. How to overcome this? Should I use useSelector in the useState? I rather use redux than the local state in this case.], but redux gives me this issue that I am not sure how to overcome.
So I am fetching my data in the useEffect hook. This should also update the redux' state (which it does).
useEffect(() => {
FetchService.fetch()
.then((res) => dispatch(fetch(res)))
.catch((err) => console.log(err));
}, []);
Then I am grabbing the state using a right selector:
const data = useSelector(selectData)
At the very end I am assigning this to the object (I cannot skip this, cuz I need it), but sometimes I get an undefined error already here, cuz the data is an empty array):
const DATA_TAB_DATASOURCES = {
dataX: [data],
};
In the component itself I check conditionally if the array isn't undefined, but it seems the problem comes from the selector as with the useState it works perfectly as the re-render is automatically triggered with the change.
{data && DATA_TAB_DATASOURCES[dataX].map((c: any, i: number) =>
.....
})}
EDIT:
Reducer:
const INITIAL_STATE: any = {
data: []
};
export const reducer = (state: any = INITIAL_STATE, action: any) => {
switch (action.type) {
case FETCH: {
return {
...state,
data: action.payload,
};
}
default:
return state;
}
};
export default reducer;
selector:
const globalSelector = (state: any) => state
export const selectData = createSelector(globalSelector, (data) => {
return data
});

Why can't I update the state using Redux?

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 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

Resources