I am new to redux and I am trying to create a solely redux app. The problem I am having is that my reducer won't update my store. If I were to mutate the store in the reducer then I would see my change. I know that is bad practice, so I am trying to update it without mutating it, but when I look at the console. I am seeing no change to the state. Can someone help me figure out why the reducer isn't updating the store?
This is my action:
store.subscribe(() => {
console.log("store changed", store.getState());
});
Here is my reducer:
const fruitReducer = function(state={
fruits: [
{
"itemName": "banana",
"price": 1.00,
"quantityRemaining": 10
},
{
"itemName": "apple",
"price": 2.00,
"quantityRemaining": 5
},
{
"itemName": "raspberry",
"price": 5.00,
"quantityRemaining": 2
},
{
"itemName": "kiwi",
"price": 3.00,
"quantityRemaining": 15
},
{
"itemName": "pineapple,
"price": 7.00,
"quantityRemaining": 1
},
{
"itemName": "strawberries",
"price": 2.00,
"quantityRemaining": 3
}
]
}, action){
if(action.type === "DEDUCT"){
return Object.assign({}, state, {
fruits: state.fruits.map((fruit, index) => {
action.payload.map((actionFruit) => {
if(fruit.itemName === actionFruit.itemName){
let newQuantity = fruit.quantityRemaining - actionFruit.quantityRemaining;
return Object.assign({}, fruit, {
quantityRemaining: newQuantity
});
}
});
return fruit;
})
});
}
else
return state;
}
Below is an example of my dispatchers(I created two doing the same thing):
store.dispatch({type: "DEDUCT", payload: [
{
"itemName": "banana",
"quantityRemaining": 1
},
{
"itemName": "apple",
"quantityRemaining": 1
},
{
"itemName": "strawberries",
"quantityRemaining": 1
}
]});
One issue I see is that you're not actually returning the result of action.fruits.map(). Arrow functions allow you to omit the return keyword if you don't use curly braces, but once you add curlies, you've started the body of a function just like normal, and it's up to you to explicitly return something.
Also, as a stylistic note, I'd suggest defining the initial state for that reducer as a separate variable:
const initialState = [ /* fruits here */];
const fruitReducer = (state = initialState, action) => {
// reducer logic here
}
It looks like your nested update logic is on the right track, but you may want to read through the Structuring Reducers - Immutable Update Patterns section of the Redux docs as well.
I found that this can happen by the way you compose your middleware. For instance, I previously had:
const store = createStore(
rootReducer,
applyMiddleware(epicMiddleware),
composeEnhancers(applyMiddleware(...middleware))
)
However, it seems as though the double apply middleware made redux grumpy, and it wouldn't catch new state updates from the rootReducer, just the epicMiddleware (which is a fancy thing to trigger actions/reducers from side effects).
Moving my epicMiddleware into my applyMiddleware(...middleware) call resolved the issues. That is, updating to the following worked:
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(...middleware)) // epicMiddleware is now in the `middleware` array
)
It may not be your problem, but it is one thing that can cause your described symptom.
Related
I'm new for the RTK Query for redux.
What's the different for auto generated hook in below two ways.
The first way look like correct from the docs but it return 304 network status.
Second way, return 200. working perfectly
1.
const ProjectsList = () => {
const {
data: projects,
isLoading,
isSuccess,
isError,
error,
} = useGetProjectsQuery("projectList") // -- return 304 network status
}
worked fine. but cannot retrieve the object from the store. return.
const {
data: projects,
isLoading,
isSuccess,
isError,
error,
} = useGetProjectsQuery() // -- return 200 network status
Third, the memoized return uninitialize. It seem didn't correct.
// ApiSlice status return uninitialize
import { createSelector, createEntityAdapter } from "#reduxjs/toolkit"
import { apiSlice } from "#/app/api/apiSlice"
const projectsAdapter = createEntityAdapter({})
export const projectsApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getProjects: builder.query({
query: () => "/api/projects",
validateStatus: (response, result) => {
return response.status === 200 && !result.isError
},
transformResponse: (responseData) => {
const loadedProjects = responseData.map((project) => {
project.id = project._id
return project
})
return projectsAdapter.setAll(initialState, loadedProjects)
},
providesTags: (result, error, arg) => {
if (result?.ids) {
return [
{ type: "Project", id: "LIST" },
...result.ids.map((id) => ({ type: "Project", id })),
]
} else return [{ type: "Project", id: "LIST" }]
},
}),
}),
})
export const {
useGetProjectsQuery,
} = projectsApiSlice
export const selectProjectsResult =
projectsApiSlice.endpoints.getProjects.select()
// creates memoized selector
const selectProjectsData = createSelector(
selectProjectsResult,
(projectsResult) => {
console.log("projectsResult: ", projectsResult) // -> { isUninitialized: true, status: "uninitialize" }
return projectsResult.data
}
)
export const {
selectAll: selectAllProjects,
selectById: selectProjectById,
selectIds: selectProjectIds,
} = projectsAdapter.getSelectors(
(state) => selectProjectsData(state) ?? initialState
)
Since your query function is just query: () => "/api/projects" (so, not using the argument in any way), both will make exactly the same request for the same resource.
There is no difference between them and every difference you see is probably something random happening on the server and not bound to either invocation.
As for retrieving from the store, there is a difference however.
Your code
export const selectProjectsResult =
projectsApiSlice.endpoints.getProjects.select()
creates a selector for the cache entry that is created calling useGetProjectsQuery() - if you wanted the cache entry for useGetProjectsQuery("foo"), that would need to be projectsApiSlice.endpoints.getProjects.select("foo").
Please note that there should almost never be any reason to use those selectors with React components - those are an escape hatch if you are not working with React. If you are working with React, use the useGetProjectsQuery hook with selectFromResult.
I am seeing people use select in this fashion quite often recently and I assume this traces back to a tutorial that misunderstood the feature - did you learn that in a tutorial and could you share that tutorial? Maybe I can convince the author to change that part.
I am trying to append action.payload to my state. However, push methods adds action.payload.length to my state instead of appending the entire array! What am I doing wrong?
const initialState = { users: [] };
export const usersSlice = createSlice({
//other code.
,
extraReducers(builder) {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
console.log(current(state));
state.users = state.users.push(...action.payload);
console.log(action.payload);
console.log(current(state));
// this one works.
// state.users = state.users.concat(action.payload);
});
},
});
// selector
export const selectUserById = (state, userId) =>
state.users.users.find((user) => user.id === userId);
This is the error I get (referring to selector):
TypeError: state.users.users.find is not a function
And this is my console:
// This is my state.
{
"users": []
}
// this is action.payload. Which is an array of 3 objects.
[
{
obj1
},
{
obj2
},
{
obj3
}
]
// This is the state after using push:
{
"users": 3
}
Well, such a silly mistake.
As y'all know, push method does not return anything. Thus, it made no sense for me to try to assign it to my state. concat method on the other hand, returns a new array. That's why it worked.
Here is what I changed:
state.users.push(...action.payload);
There is not state.users = anymore.
I'm building an app where a "slice reducer" needs to access state of another "slice reducer". The redux docs talks about using a custom combine reducer in order to pass in the root state to the reducer - Beyond combineReducers
Thus far, I have this for my root reducer:
import cats from '../slices/cats'
import dogs from '../slices/dogs'
import status from '../slices/status'
function combinedReducer(state = {}, action) {
return {
status: status(state.status, action),
dogs: dogs(state.dogs, action),
cats: cats(state.cats, action, state),
};
}
export default configureStore({ reducer: combinedReducer });
I don't seem to be able to get the root state for my cats reducer - passed in as the 3rd arg above.
const assetsSlice = createSlice({
name: 'cats',
initialState,
reducers: {
setFetched: (state, { payload }, root) => {
// root is undefined
state.type = payload + root.dogs.legs;
},
},
});
This should work, no?
If I use a vanilla reducer that's not created by createSlice I am able to get the root state
export default (state = initialState, action, root) => {
// root - { status: {}, dogs: {}, cats: {} }
};
This is not possible as a third argument since RTK's reducers only pass the first two arguments to the case reducers.
You could just add it to the action though (but granted, that's hacky):
function combinedReducer(state = {}, action) {
const actionWithFullState = { ...action, meta: {...action.meta, fullState: state }}
return {
status: status(state.status, action),
dogs: dogs(state.dogs, action),
cats: cats(state.cats, actionWithFullState),
};
}
I've looked at a bunch of examples of working with nested reducers, but I'm having a very strange problem.
I have the following initial state:
window.store = configureStore({
slider: {
mainImageIndex: 0,
pageNum: 1,
perPage: 4, // Per supplied requirements
},
});
The following in my index.js reducer (excluding all the imports):
export default combineReducers({
searchPhotos,
slider: combineReducers({
mainImageIndex: setMainImage,
pageNum: nextPage,
perPage: setPerPage,
}),
form: reduxFormReducer, // mounted under "form"
});
And my setMainInage.js reducer:
export default (state = {}, action) => {
switch (action.type) {
case 'SET_MAIN_IMAGE':
return {
...state,
mainImageIndex: action.mainImageIndex,
};
default:
return state;
}
};
Attached are before and after screen grabs of the redux devtools. Notice that after calling the SET_MAIN_IMAGE, the hierarcy inside the slider node changes. For some reason instead of just updating the mainImageIndex, it's nesting a new mainImageIndex key under the original. Anyone know what could be causing that?
The thing you have to check is what does your action contain ?
It seems that it contains something like that
{
type: 'SET_MAIN_IMAGE',
mainImageIndex: {
mainImageIndex: 2
},
}
Your reducer will be like
1. First step
case 'SET_MAIN_IMAGE':
return {
...state,
mainImageIndex: action.mainImageIndex,
};
2. Second step
case 'SET_MAIN_IMAGE':
return {
mainImageIndex: 0,
pageNum: 1,
perPage: 4,
mainImageIndex: action.mainImageIndex,
};
3. Third step
case 'SET_MAIN_IMAGE':
return {
mainImageIndex: 0,
pageNum: 1,
perPage: 4,
mainImageIndex: {
mainImageIndex: 2
},
};
4. Fourth step
case 'SET_MAIN_IMAGE':
return {
pageNum: 1,
perPage: 4,
mainImageIndex: {
mainImageIndex: 2
},
};
What you should do
Change the action payload in order to send something like that
{
type: 'SET_MAIN_IMAGE',
mainImageIndex: 2
}
or better
{
type: 'SET_MAIN_IMAGE',
payload: {
mainImageIndex: 2,
}
}
then use in your reducer
case 'SET_MAIN_IMAGE':
return {
...state,
...action.payload,
};
Hope it helps.
So I was able to solve the problem by creating a new reducer for the slider, and put all actions inside that one reducer, instead of having one function per file. Then I could just do:
export default combineReducers({
searchPhotos,
slider,
form: reduxFormReducer, // mounted under "form"
});
I think there must still be a way to do it with multiple files, but maybe it does make sense to put all the actions for one part of state in the same reducer.
Maybe I'm missing something completely obvious but this has been tripping me up today.
Let's say we have a Redux store with a structure like so:
const state = {
...
pages: {
...
accountPage: {
currentTab: 'dashboard',
fetching: false,
tableSettings: {
sortDir: 'asc',
sortField: 'name'
}
}
}
}
So there is obviously a main reducer...
export default combineReducers({
...
pages: pagesReducer
...
});
Then the reducer for pages has the reducer for each page...
export default combineReducers({
...
accountPage: accountPageReducer
...
});
And now finally we get down to the meat of the problem, the reducer for this particular piece of state.
export default handleActions({
[setCurrentTab]: (state, action) => { ... },
[setIsFetching]: (state, action) => { ... }
});
That's all good right? Well, the key in the state given at the outset at tableSettings should actually be handled by it's own reducer. This pattern may exist many times in the state, so it is abstracted away to a reducer-creating function:
const defaultState = {
sortDir: 'asc',
sortField: null
};
export const createTableSettingReducer (actions, extra ={}) => {
return handleActions({
[actions.changeSortDir]: (state, action) => ({ ...state, sortDir: action.payload }),
[actions.changeSortField]: (state, action) => ({ ...state, sortField: action.payload }),
...extra
}, defaultState)
}
So, above the reducer for the sections of state (accountPageReducer), we created the reducer:
// pretend these actions were imported
const tableSettingsReducer = createTableSettingReducer({
changeSortDir: setSortDir,
changeSortField: setSortField
});
So the question is, where do I put tableSettingsReducer?
This of course, doesn't work:
export default handleActions({
[setCurrentTab]: (state, action) => { ... },
[setIsFetching]: (state, action) => { ... },
tableSettings: tableSettingsReducer
});
It doesn't work because handleActions expects to use the action constants as keys, not the actual key in the state.
There is also nowhere to use combineReducers, since there is only one nested reducer of this slice of state. currentTab and fetching do not need their own reducer, so it's fruitless to use combineReducers.
I know that recently redux-actions started support nested reducers...but there isn't really any documentation available showing exactly how it's supposed to be done, or even describing the parameters needed to make it happen.
I could possibly use combineActions, and combine all of the actions in handleActions for every action that can be taken by a nested reducer. But that doesn't seem very clean...plus, what if the nested reducer has it's own nested reducers? That means every time those reducers can process a new action, that action needs to be added to combineActions in all its parents. Not the best.
Thoughts?
Every key in your state gets its own reducer. Some reducers are really simple, some are themselves composed of other reducers. All the sister keys at each level of your state tree can be combined with combineReducers.
const initialCurrentTab = 'dashboard';
const currentTabReducer = handleActions({
[setCurrentTab]: (state, action) => {
return action.payload;
},
}, initialCurrentTab);
const defaultFetchingState = false;
const fetchingReducer = handleActions({
[setIsFetching]: (state, action) => {
return action.payload;
},
}, defaultFetchingState);
export default combineReducers({
currentTab: currentTabReducer,
fetching: fetchingReducer,
tableSettings: tableSettingsReducer,
});
let say you have the initialState = { data : []}
let assume that the upcoming action has payload of an array
export the reducer as the following :
return handleActions({
["Action Type 1" ]: (state, { payload }) => {
return { ...state, data: [...state.data, ...payload ]} ;
},
["Action Type 1" ]: (state, { payload }) => {
return { ...state, data: [...state.data, ...payload ]} ;
},
}, initialSate );
import this reducer in your combine reducer .