extraReducers in createSlice() in Redux Toolkit - redux

Here's the example code from Codecademy:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import { client } from '../api';
const initialState = {
todos: [],
status: 'idle'
};
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/todosApi/todos');
return response.todos;
});
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action) => {
state.todos.push(action.payload);
}
},
extraReducers: {
[fetchTodos.pending]: (state, action) => {
state.status = 'loading';
},
[fetchTodos.fulfilled]: (state, action) => {
state.status = 'succeeded';
state.todos = state.todos.concat(action.payload);
}
}
});
I'm confused about what fetchTodos.pending and fetchTodos.fulfilled mean as computed properties. I don't see that fetchTodos has those attributes. What is going on?

Those are generated by createAsyncThunk
Check out the RDK docs on createAsyncThunk
Parameters#
createAsyncThunk accepts three parameters: a string action type value, a payloadCreator callback, and an options object.
type
A string that will be used to generate additional Redux action type
constants, representing the lifecycle of an async request:
For example, a type argument of 'users/requestStatus' will generate these
action types:
pending: 'users/requestStatus/pending'
fulfilled: 'users/requestStatus/fulfilled'
rejected: 'users/requestStatus/rejected'

Related

How to add cases in ExtraReducer to match the actions created in currentReducer using createSlice() from #reduxjs/toolkit

Here below I have mentioned a redux slice. A fetchAllApps thunk function is created with createAsyncThunk for action 'allApps/allappsAdded/' which I dynamically got by allAppsAdded.type. When the fetchAllapps is dispatched it generated actions of type 'allApps/allappsAdded/pending', 'allApps/allappsAdded/fulfilled', 'allApps/allappsAdded/rejected' which I need to add in extraReducers to handle it by doing hardcode.Is there any way to add these action types like allAppsAdded.type programatically?. so that in future It makes easy for me to change these without redundant..
import {
configureStore,
createAsyncThunk,
createSlice
} from "#reduxjs/toolkit";
const initialState = {
apps: [],
categories: [],
loading: {
apps: false
}
};
const allappsSlice = createSlice({
name: "allapps",
initialState,
reducers: {
allappsAdded: (state, action) => {
state["apps"] = action.payload.apps;
state["categories"] = action.payload.categories;
}
},
extraReducers: {
}
});
export default () =>
configureStore({
reducer: allappsSlice.reducer
});
const { allappsAdded } = allappsSlice.actions;
const fetchAllApps = createAsyncThunk(allappsAdded.type, async () => {
console.log("ss");
setTimeout(() => ({ apps: [], categories: [] }), 2000);
});
export { allappsAdded, fetchAllApps };

Why calling thunk is specific from app.tsx?

When I call me thunk from app.tsx, it returns this error:
> Build error occurred
TypeError: Cannot read property 'useContext' of null
at exports.useContext (/Users/kukodajanos/Workspace/Tikex/Portal/Team/node_modules/react/cjs/react.production.min.js:24:118)
at useReduxContext (/Users/kukodajanos/Workspace/Tikex/Portal/Team/node_modules/react-redux/lib/hooks/useReduxContext.js:27:46)
Why app.tsx is specific to all other component?
import { useAppDispatch } from '../hooks'
import { me } from '../tikexModule/slices/tikexAPI'
const MyApp: Page = ({ Component, pageProps }: AppPropsWithLayout) => {
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(me())
}, [])
return <></>
}
export default MyApp
And the thunk itself:
export const me = createAsyncThunk(`${namespace}/me`, async () => {
const { data } = await axios({
method: 'get',
url: 'me',
headers: { crossDomain: true },
})
return data
})
and the action:
const tikexAPI = createSlice({
name: 'tikexAPI',
initialState,
reducers: {
},
extraReducers: (builder) => {
builder
.addCase(me.fulfilled, (state, { payload }) => {
state.authnRes = payload
})
and hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
import { createSelector } from '#reduxjs/toolkit'
import {
authnResS,
userDTOS,
organizationsS,
selectedOrganizationIdS,
selectedOrganizationS,
selectedEventIdS,
selectedEventS,
} from './tikexModule/slices/tikexAPI'
import {
AuthnRes,
OrganizationDTO,
OrganizationList,
SoldTicketRes,
UserDTO,
ProgramDTO,
} from './tikexModule/Types'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>()
I believe the error is in dispatch, where you dispatch a function call of the thunk, instead of passing the thunk function itself.
const thunkFunction = (dispatch, getState) => {
// logic here that can dispatch actions or read state
}
store.dispatch(thunkFunction)
Note that thunkFunction has no parenthesis () to invoke the thunk.
In your case me() should be without parenthesis:
useEffect(() => {
dispatch(me)
}, [])
I don't work with thunks normally but I believe the reason that works is that a thunk has a toString() function that exposes the action type `${namespace}/me`.

Redux and Firebase

I've struggled to implement react-redux-firebase and redux-firestore into my app after configuring the redux store (struggled with this too, even though redux-toolkit simplified some things). Is it possible that I can communicate with firebase without using those two packages above? If so, how do I use firebase in any of my slices? e.g., auth slice below.
import {createSlice, createAsyncThunk} from '#reduxjs/toolkit';
import firebase from 'firebase/app';
export const authSlice = createSlice({
name: 'authSlice',
initialState: {
currentUser: null,
isLoggedIn: false,
isLoading: false,
},
reducers: {
login: async (state, action) => {},
registerUser: (state, action) => {},
changeProfile: (state, action) => {},
logout: async (state, action) => {},
setCurrentUser: (state, action) => {},
},
});
// Action creators are generated for each case reducer function
export const {
login,
registerUser,
changeProfile,
logout,
setCurrentUser,
} = authSlice.actions;
export default authSlice.reducer;
This is the query in a separate file.
import firestore from '#react-native-firebase/firestore';
export const getPopularProducts = firestore()
.collection('POPULAR')
.orderBy('count', 'desc')
.limit(10)
.get()
.then(querySnapshot => {
const views = [];
querySnapshot.forEach(doc => {
views.push({
key: doc.id,
count: doc.data().count,
product: doc.data().product,
});
});
return views;
})
.catch(error => {
alert('Error getting popular products: ', error);
});
In the reducer/slice, import getPopularProducts.
import {createSlice, createAsyncThunk} from '#reduxjs/toolkit';
import {getPopularProducts} from './../../lib/fetchData';
// Initial states
const initialState = {
products: [],
mainList: [],
popular: [],
};
// Get popular products from firebase
export const fetchPopularProducts = createAsyncThunk(
'prodSlice/fetchPopularProducts',
async () => {
const data = getPopularProducts;
const {_W} = data;
if (_W !== null) {
return _W;
}
},
);
export const productSlice = createSlice({
name: 'prodSlice',
initialState,
reducers: {
fetchData: (state, action) => {
state.isLoading = true;
state.mainList = action.payload;
state.products = action.payload;
}
},
extraReducers: {
[fetchPopularProducts.fulfilled]: (state, action) => {
state.popular = action.payload;
},
},
});
// Action creators are generated for each case reducer function
export const {fetchData} = productSlice.actions;
export const selectProducts = state => state.prodSlice;
export default productSlice.reducer;
Then you dispatch fetchPopularProducts inside the useEffect hook. I cases where I needed a parameter for the query, I'd put the query inside createAsyncThunk.

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;

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