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

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) => {}
)

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

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;

Redux-Actions handleActions Nested Reducers

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 .

Allow partial type

Using Flowtype together with Redux, I have a type like this:
export type MapState = {
addresses: Address[],
selected: Array<number>
}
and an action creator:
export const setParams = (params: any): Action => {
return { type: actionTypes.SET_PARAMS, payload: { params };
}
In the reducer, I merge the params into the state:
export default (state: MapState = initialState, action: SetParamsAction) => {
switch (action.type) {
case actionTypes.SET_PARAMS: {
return {
...state,
...action.payload.params
}
[...]
I'm looking for a possibility to tell Flowtype to accept params in the action creator, if it is an object consisting only of properties of MapState, so that I can get rid of the any in setParams. Any idea?
You can just add a exact PossibleParams Object type like so:
type PossibleParams = {|
addresses?: Address[],
selected?: number[],
|};
export const setParams = (params: PossibleParams): Action => ({
type: actionTypes.SET_PARAMS,
payload: {
params,
},
});
You can check all the possibilities on flow.org/try 🙂

Resources