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!
Related
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
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 have a list of items on my application and I am trying to create a details page to each one on click. Moreover I am not managing how to do it with useState and useEffect with typescript, I could manage just using componentDidMount and is not my goal here.
On my state, called MetricState, I have "metrics" and I have seen in some places that I should add a "selectedMetric" to my state too. I think this is a weird solution since I just want to be able to call a dispatch with the action of "select metric with id x on the metrics list that is on state".
Here is the codesandbox, when you click on the "details" button you will see that is not printing the name of the selected metric coming from the state: https://codesandbox.io/s/react-listing-forked-6sd99
This is my State:
export type MetricState = {
metrics: IMetric[];
};
My actionCreators.js file that the dispatch calls
export function fetchMetric(catalog, metricId) {
const action = {
type: ACTION_TYPES.CHOOSE_METRIC,
payload: []
};
return fetchMetricCall(action, catalog, metricId);
}
const fetchMetricCall = (action, catalog, metricId) => async (dispatch) => {
dispatch({
type: action.type,
payload: { metrics: [catalog.metrics.filter((x) => x.name === metricId)] } //contains the data to be passed to reducer
});
};
and finally the MetricsDeatail.tsx page where I try to filter the selected item with the id coming from route parametes:
const metricId = props.match.params.metricId;
const catalog = useSelector<RootState, MetricState>(
(state) => state.fetchMetrics
);
const selectedMetric: IMetric = {
name: "",
owner: { team: "" }
};
const dispatch = useDispatch();
useEffect(() => {
dispatch(appReducer.actionCreators.fetchMetric(catalog, metricId));
}, []);
On my state, called MetricState, I have "metrics" and I have seen in some places that I should add a "selectedMetric" to my state too. I think this is a weird solution since I just want to be able to call a dispatch with the action of "select metric with id x on the metrics list that is on state".
I agree with you. The metricId comes from the URL through props so it does not also need to be in the state. You want to have a "single source of truth" for each piece of data.
In order to use this design pattern, your Redux store needs to be able to:
Fetch and store a single Metric based on its id.
Select a select Metric from the store by its id.
I generally find that to be easier with a keyed object state (Record<string, IMetric>), but an array works too.
Your component should look something like this:
const MetricDetailsPage: React.FC<MatchProps> = (props) => {
const metricId = props.match.params.metricId;
// just select the one metric that we want
// it might be undefined initially
const selectedMetric = useSelector<RootState, IMetric | undefined>(
(state) => state.fetchMetrics.metrics.find(metric => metric.name === metricId )
);
const dispatch = useDispatch();
useEffect(() => {
dispatch(appReducer.actionCreators.fetchMetric(metricId));
}, []);
return (
<div>
<Link to={`/`}>Go back to main page</Link>
Here should print the name of the selected metric:{" "}
<strong>{selectedMetric?.name}</strong>
</div>
);
};
export default MetricDetailsPage;
But you should re-work your action creators so that you aren't selecting the whole catalog in the component and then passing it as an argument to the action creator.
I have a store with
const initialState = {
users: dummyData,
is_created: false
};
where users is an array. Then a getUsers selector const getUsers = state => state.users;
Inside the component I used the useSelector to get access to my state.
const { users } = useSelector(state => ({
users: getUsers(state),
}));
When I try to loop through users
for (let i = 0; i < users.length; i++) {
//...
}
I get an error Cannot read property 'length' of undefined
useSelector seems to run provided function at arbitrary time so its no guaranteed to have users defined when you want because of the async nature of it.
Couple ways you can tackle this:
First define default for users like:
const { users = [] } = useSelector(state => ({
users: getUsers(state),
}));
That should resolve your issue.
Enhancement to your above code using for loop would be not to rely on for loop instead use .map like below:
users.map(user => { ... });
I found the error from the redux dev tools chart. I am using combineReducers() and I named the reducer after the users the component name ie
export default combineReducers({
users: users.reducers,
});
So the correct selector would be
const getUsers = state => state['users'].users;
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