Redux-Actions handleActions Nested Reducers - redux

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 .

Related

createAsyncThunk isn't changing state

So, i'am trying to create async thunk action, to mock data from file to the state.
import { createAsyncThunk, createSlice, PayloadAction } from '#reduxjs/toolkit';
import mocktasks from './mocktasks';
export interface TasksState {
data: Task[];
}
const initialState: TasksState = {
data: [],
};
export const fetchTasks = createAsyncThunk('tasks/fetchTasks', () => mocktasks);
export const tasksSlice = createSlice({
name: 'tasks',
initialState,
reducers: {
setTasks: (state:TasksState, action: PayloadAction<Task[]>) => ({
...state,
data: action.payload,
}),
addTask: (state: TasksState, action: PayloadAction<Task>) => ({
...state,
data: [...state.data, action.payload],
}),
},
extraReducers: {
[fetchTasks.fulfilled.name]: (state, action: PayloadAction<Task[]>) => ({
...state,
data: action.payload,
}),
},
});
export const { setTasks, addTask } = tasksSlice.actions;
export default tasksSlice.reducer;
But there is a strange thing: even though, fetch actions have dispatched, my state hasn't changed.
State and dispatched actions
I thought, that there was an issue with passing the payload to fetchTasks/fulfilled, but Redux devtools shows, that fulfilled has right data in payload, that is obtained from mock file:Action payload
UPD: even though, using dispatch(setTasks(mocktasks)) inside createAsyncThunk works just like i need.
Use builder notation to avoid typescript errors (official recomendation from redux-toolkit docs):
extraReducers: (builder) => {
.addCase(fetchTasks.fulfilled, (state) => ({
...state,
data: action.payload,
}))
},
[fetchTasks.fulfilled.type] should work, as .name seems to always return the string actionCreator.

redux-toolkit sharing state between slice reducer

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),
};
}

Cannot read property 'ids' of undefined when use reduxjs/toolkit

I am trying to pass values from API to state but always give this error.
TypeError: Cannot read property 'ids' of undefined
selectIds
I am using the 'reduxjs/toolkit' I try everything but still continue that error could you please help me
this is a code from the Slic file
export const getListNamesDictionary = createAsyncThunk('dictionary/names/getNames', async () => {
try {
const response = await axios.get('http://localhost:6005/api/lookup/list-name');
const data = await response.data;
// dispatch(getNames(data));
debugger;
console.log(data);
return data;
} catch (error) {
return console.error(error.message);
}
});
const namesAdapter = createEntityAdapter({});
and the Slic :
const namesDictionarySlice = createSlice({
name: 'names',
initialState: {
names: []
},
reducers: {
},
extractors: {
[getListNamesDictionary.fulfilled]: (state, action) => {
state.entities.push(action.payload);
}
}
});
export const { selectAll: selectNamesDictionary } = namesAdapter.getSelectors(state => state.data);
and this code from component where I need to dispatch the action
const names = useSelector(selectNamesDictionary);
useEffect(() => {
// dispatch(getListNamesDictionary()).then(() => setLoading(false));
dispatch(getListNamesDictionary()).then(() => setLoading(false));
}, [dispatch]);
any suggesting why that error? and thanks
You are not using the entity adapter properly. It expects to manage a state in the form:
{
ids: [1, 2],
entities: {
1: {/*...*/},
2: {/*...*/}
}
}
Your names slice doesn't match that shape. But that's an easy fix as the namesAdapter provides all of the needed tools. Quick rundown of errors to fix:
property name extractors should be extraReducers
state.entities.push needs to be replaced with an adapter function
initialState needs to have properties ids and entities
selectors need to target the correct location
const namesAdapter = createEntityAdapter({});
const namesDictionarySlice = createSlice({
name: "names",
initialState: namesAdapter.getInitialState(),
reducers: {},
extraReducers: {
[getListNamesDictionary.fulfilled]: namesAdapter.upsertMany
}
});
This fixes the first three bullets. Regarding the reducer, it might make more sense if you write it out like this, but it does the same thing.
[getListNamesDictionary.fulfilled]: (state, action) => {
namesAdapter.upsertMany(state, action)
}
The last bullet point is the cause of the specific error message the you posted:
TypeError: Cannot read property 'ids' of undefined
It actually seems like state.data is undefined. Is this namesDictionarySlice being used to control the data property of your root state? If it is something else, like state.names, then you need to change your selectors to namesAdapter.getSelectors(state => state.names).
If your store looks like this:
const store = configureStore({
reducer: {
names: namesReducer
}
});
You would want:
export const { selectAll: selectNamesDictionary } = namesAdapter.getSelectors(
(state) => state.names // select the entity adapter data from the root state
);
in Slic function, I make a mistake in writing, I most write 'extraReducer' but I wrote "extractors" :D

Do actions added with extraReducers on createSlice have the slice's name prefix added to their types?

From the official doc's example:
https://redux-toolkit.js.org/api/createSlice#the-extrareducers-builder-callback-notation
import { createAction, createSlice } from '#reduxjs/toolkit'
const incrementBy = createAction<number>('incrementBy')
const decrement = createAction('decrement')
createSlice({
name: 'counter',
initialState: 0,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(incrementBy, (state, action) => { // DO SOMETHING })
.addCase(decrement, (state, action) => { // DO SOMETHING })
.addDefaultCase((state, action) => {})
},
})
Also from the docs:
One of the key concepts of Redux is that each slice reducer "owns" its slice of state, and that many slice reducers can independently respond to the same action type. extraReducers allows createSlice to respond to other action types besides the types it has generated.
QUESTION
In the example above, will the cases incrementBy and decrement also get the counter name as a prefix in their types?
Like:
"counter/incrementBy"
"counter/decrement"
Is this how the extraReducers property work?
No, because the entire point of extraReducers is that it does not generate any new action types.
extraReducers exists so that a slice reducer can listen to other action types that have already been defined outside the slice.
No. It does not get the name prefix.
https://codesandbox.io/s/xenodochial-dew-35ivq
import { createAction, createSlice } from "#reduxjs/toolkit";
interface CounterState {
value: number;
}
export const decrementV2 = createAction('decrement');
const initialState = { value: 0 } as CounterState;
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state,action) {
console.log(`action.type: ${action.type}`);
state.value++;
},
decrement(state,action) {
console.log(`action.type: ${action.type}`);
state.value--;
}
},
extraReducers: (builder) => {
builder.addCase(decrementV2, (state, action) => {
console.log("FROM decrementV2 (from extraReducers)")
console.log(`action.type: ${action.type}`);
state.value--;
});
}
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

How does one have multiple cases that run the same code with Redux Toolkit?

I'm working on converting some older Redux code to use the new Redux Toolkit. I've run into a problem where, in the old code, multiple case statements would trigger the same reducer logic. How does one do this with the new case reducer functions?
In the old code, REGISTER_FAIL, AUHT_ERROR, LOGIN_FAIL, LOGOUT all run the same code. Is it possible to have this same type scenario in the createSlice reducers object?
Old Code
case REGISTER_FAIL:
case AUTH_ERROR:
case LOGIN_FAIL:
case LOGOUT:
localStorage.removeItem('token');
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
};
default:
return state;
New Code
const authUserSlice = createSlice({
name: 'authUser',
initialState,
reducers: {
registerFail(state, action) {
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
};
},
registerSuccess
},
});
There's a couple different options.
First, you could write the case reducer functions separately, then pass them to createSlice multiple times to generate corresponding actions for each field name:
function resetState() {
Object.assign(state, {
token: null,
// etc
});
}
const authUserSlice = createSlice({
name: 'authUser',
initialState,
reducers: {
registerFailed: resetState,
logout: resetState,
// etc
}
});
The other option is to use the extraReducers field, and use builder.addMatcher() to handle multiple cases with the same reducer:
const authUserSlice = createSlice({
name: 'authUser',
initialState,
reducers: {
// omit
},
extraReducers: builder => {
builder.addMatcher(
// can pass multiple RTK action creators here
isAnyOf(registerFailed, loginFailed),
(state, action) => {
// reset state here
}
)
}
});
If you're still interoping with old code and have old-style action constants like const LOGIN_FAILED = "LOGIN_FAILED" and need to match those, you can write your own "matching" function that just does a string type comparison, like:
builder.addMatcher(
(action) => [LOGIN_FAILED, REGISTER_FAILED].includes(action.type),
(state, action) => {}
)

Resources