Catch all Pending or Rejected actions within a redux-toolkit slice - redux

Aight.. so im pretty new with redux toolkit and I want to catch ALL pending actions in one slice to basically show a loading modal. I know we can do this with redux-saga and probably redux-observable
Soooooo instead of
builder.addCase(fetchUsers.pending, (state) => {
state.loading = LoadingState.PENDING;
});
To Something like this
builder.addCase(allActions.pending, (state) => {
state.loading = LoadingState.PENDING;
});
I know allActions does not work there but is there anything that will.

You can use the matching utilities included in RTK:
import { createSlice, isPending} from "#reduxjs/toolkit";
const dataSlice = createSlice({
name: "data",
reducers: { /* */ },
extraReducers: builder => {
builder.addMatcher(isPending, (state, action) => {
// logic here
})
}
})
You can also combine the matching utilities in various ways to only handle the pending state for specific thunks, etc.

Related

changing state with RTK Query

I'm learning about RTK Query and really confused. I'd be happy if someone could point me towards the right direction. My question is how one can manipulate the state of the application store the same way as it is done when using createAsyncThunk and setting up extraReducers.
export const asyncApiCall = createAsyncThunk("api/get_data", async (data) => {
const config = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
};
const res = await axios.get( "http://apiserver/path_to_api",data,config );
return res['data']
} )
export const mySlice = createSlice({
name:"mySliceName",
initialState:{
data: [],
loadInProgress: false,
loadError: null,
extraData: {
// something derived based on data received from the api
}
},
extraReducers: {
[asyncApiCall .pending]: (state) => {
state.loadInProgress = true;
},
[asyncApiCall .fulfilled]: (state,action) => {
state.loadInProgress = false;
state.data = action.payload;
state.extraData = someUtilFunc(state.data)
},
[asyncApiCall.rejected]: (state) => {
state.loadInProgress = false;
state.loadError= true;
},
}
})
Now I'm replacing it with RTK Query. My current understanding is that RTK Query automatically generates hooks for exposing data received from the api and all the query-related info like if it's pending, if an error occurred etc.
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react'
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: builder => ({
getData: builder.query({
query: () => '/get_data'
}),
setData: builder.mutation({
query: info => ({
url: '/set_data',
method: 'POST',
body: info
})
})
})
})
export const { useSendDataMutation, useGetDataQuery } = apiSlice
If I want to store some additional data that may be affected by the api calls should I create another slice that will somehow interact with the apiSlice, or is it possible to incorporate everything in this existing code? I'm sorry for possible naivety of this question.
The short answer is that RTK Query is focused on purely caching data fetched from the server. So, by default, it stores exactly what came back in an API call response, and that's it.
There are caveats to this: you can use transformResponse to modify the data that came back and rearrange it before the data gets stored in the cache slice, and you can use updateQueryData to manually modify the cached data from other parts of the app.
The other thing to note is that RTK Query is built on top of standard Redux patterns: thunks and dispatched actions. Every time an API call returns, a fulfilled action gets dispatched containing the data. That means you can also apply another suggested Redux pattern: listening for that action in other reducers and updating more than one slice of state in response to the same action.
So, you've got three main options here:
If the "extra data" is derived solely from the server response values, you could use transformResponse and return something like {originalData, derivedData}
You could just keep the original data in the cache as usual, but use memoized selector functions to derive the extra values as needed
If you might need to update the extra values, then it's probably worth looking at listening to a query fulfilled action in another slice and doing something with it, like this silly example:
import { api } from "./api";
const someExtraDataSlice = createSlice({
name: "extraData",
initialState,
reducers: {/* some reducers here maybe? */},
extraReducers: (builder) => {
builder.addMatcher(api.endpoints.getPokemon.matchFulfilled, (state, action) => {
// pretend this field and this payload data exist for sake of example
state.lastPokemonReceived = action.payload.name;
}
}
})

slice, action and reducers, what are they?

I'm trying to learn Redux, and i encountered this code:
reducers: {
loginStart: (state) => {
//...
},
loginSuccess: (state, action) => {
//...
},
loginFailure: (state) => {
//...
},
logout: (state) => {
//...
},
},
});
export const { loginStart, loginSuccess, loginFailure, logout } = userSlice.actions;
export default userSlice.reducer;
I can't understand well what are .actions, Slice, .reducer or reducers from different web sources.
So kindly can any expert in Redux here explain in a simplified way what are theses and their roles?
Every state of your app (which is global) lives in an object tree stored in a single store.
Actions are simply JavaScript objects that have a type with a payload of data illustrating exactly what is happening. what they do? they are the only way to manage our state tree. pay attention: no state has been mutated so far.
Reducers are just responses to our corresponding called action to perform on our immutable state and thus returning a new state. optionally you might also want to check Array.reduce() method to better understand reducers.
What is slice then? as it comes with redux-toolkit, slice contains all the reducer logics and actions for a single feature.
it auto generates your action creators and types which you have to define them as constants before redux-toolkit. check createSlice for the full explanation.
In your example the object called reducers goes into your createSlice with also an initial state and a name.
Based on all that being said, this is your final example of your question:
const initialState = {}
const authSlice = createSlice({
name: 'authentication',
initialState,
reducers: {
loginStart: (state) => {
//...
},
loginSuccess: (state, action) => {
//...
},
loginFailure: (state) => {
//...
},
logout: (state) => {
//...
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

how do I migrate from redux to redux toolkit

I managed to write reducer using createSlice but the action seems to be confusing.
My old reducer :
function listPeopleReducer(state = {
getPeople:{}
}, action){
switch (action.type) {
case D.LIST_PEOPLE: {
return {
...state
, getPeople:action.payload
}
}
default:{}
}
return state
}
By using createSlice from the redux toolkit, I migrated the reducer to this,
const listPeopleReducer = createSlice({
initialState:{getPeople:{}},
name:"listPeople",
reducers:{
listPeople(state,action){
return {
...state,
getPeople : action.payload
}
}
}
})
My old action, makes an api call inside it, with the help of a helper function makeApiRequest (which takes in parameters and returns the response of the api),
export function listPeople(config: any) {
return function (dispatch: any) {
makeApiRequest(config)
.then((resp) => {
dispatch({
type : D.LIST_PEOPLE,
payload : resp.data
})
})
.catch((error) => {
dispatch({
type : D.LIST_PEOPLE,
payload : error
})
})
}
}
With reduxtool kit, we could do something like,
const listPeople = listPeopleReducer.actions.listPeople;
But, how will I write my custom action that contains the helper function makeApiRequest ?
i.e The old Action should be migrated to reduxtoolkit type.
It's definitely tricky when migrating, since there are some major conceptual changes that you must eventually wrap your head around. I had to do it a couple of times before it clicked.
First, when you are creating const listPeopleReducer with createSlice(), that is not actually what you are creating. A slice is a higher level object that can generate action creators and action types for you, and allows you to export reducers and actions FROM it.
Here are the changes I would make to your code:
const peopleSlice = createSlice({
initialState:{getPeople:{}},
name:"people",
reducers:{
listPeople(state,action){
// uses immer under the hood so you can
// safely mutate state here
state.getPeople = action.payload
}
},
extraReducers:
// each thunk you create with `createAsyncThunk()` will
// automatically have: pending/fulfilled/rejected action types
// and you can listen for them here
builder =>
builder.addCase(listPeople.pending, (state,action) => {
// e.g. state.isFetching = true
})
builder.addCase(listPeople.fulfilled, (state,action) => {
// e.g. state.isFetching = false
// result will be in action.payload
})
builder.addCase(listPeople.rejected, (state,action) => {
// e.g. state.isFetching = false
// error will be in action.payload
})
}
})
Then, outside of your slice definition, you can create actions by using createAsyncThunk(), and do like:
export const listPeople = createAsyncThunk(
`people/list`,
async (config, thunkAPI) => {
try {
return makeApiRequest(config)
} catch(error) {
return thunkAPI.rejectWithError(error)
// thunkAPI has access to state and includes
// helper functions like this one
}
}
}
The "Modern Redux with Redux Toolkit" page in the Redux Fundamentals docs tutorial shows how to migrate from hand-written Redux logic to Redux Toolkit.
Your makeApiRequest function would likely be used with Redux Toolkit's createAsyncThunk, except that you should return the result and let createAsyncThunk dispatch the right actions instead of dispatching actions yourself.

Calling other actions from createAsyncThunk

Usually in a thunk you'd wind up calling other actions:
const startRecipe = {type: "startRecipe"}
const reducer = (state, action) => {
if (action.type === "startRecipe") {
state.mode = AppMode.CookRecipe
}
}
const getRecipeFromUrl = () => async dispatch => {
const res = await Parser.getRecipeFromUrl(url)
dispatch(startRecipe)
}
With createAsyncThunk in redux toolkit, this isn't so straightforward. Indeed you can mutate the state from your resulting action in extraReducers:
export const getRecipeFromUrl = createAsyncThunk('getRecipeFromUrl',
async (url: string): Promise<RecipeJSON> => await Parser.getRecipeFromUrl(url)
)
const appStateSlice = createSlice({
name: 'app',
initialState: initialAppState,
reducers: {},
extraReducers: ({ addCase }) => {
addCase(getRecipeFromUrl.fulfilled, (state) => {
state.mode = AppMode.CookRecipe
})
}
})
But I also want to have non-async ways to start the recipe, which would entail a reducer in the slice:
reducers: {
startRecipe(state): state.mode = AppState.CookRecipe
},
To avoid writing the same code in two places I would love to be able to call the simple reducer function from the thunk handler. I tried simply startRecipe(state) and startRecipe (which had been destructured for ducks exporting so I’m fairly sure I was referring to the correct function) from the extraReducers case but it doesn't work.
My current solution is to define _startRecipe outside of the slice and just refer to that function in both cases
reducers: { startRecipe: _startRecipe },
extraReducers: builder => {
builder.addCase(getRecipeFromUrl.fulfilled, _startRecipe)
}
Is there a "better" way where you can define the simple action in your slice.reducers and refer to it from the thunk handler in extraReducers?
The second argument of the payloadCreator is thunkAPI (doc) from where you could dispatch the cookRecipe action.
interface ThunkApiConfig {
dispatch: AppDispatch,
state: IRootState,
}
export const getRecipeFromUrl = createAsyncThunk('getRecipeFromUrl',
async (url: string, thunkAPI: ThunkApiConfig): Promise<RecipeJSON> => {
await Parser.getRecipeFromUrl(url)
return thunkAPI.dispatch(cookRecipeActionCreator())
}
)
The idea of "calling a reducer" is the wrong approach, conceptually. Part of the design of Redux is that the only way to trigger a state update is by dispatching an action.
If you were writing the reducer using a switch statement, you could have multiple action types as cases that all are handled by the same block:
switch(action.type) {
case TypeA:
case TypeB: {
// common logic for A and B
}
case C: // logic for C
}
When using createSlice, you can mimic this pattern by defining a "case reducer" function outside of the call to createSlice, and pass it for each case you want to handle:
const caseReducerAB = (state) => {
// update logic here
}
const slice = createSlice({
name: "mySlice",
initialState,
reducers: {
typeA: caseReducerAB,
typeB: caseReducerAB,
}
extraReducers: builder => {
builder.addCase(someAction, caseReducerAB)
}
})
That sounds like what you described as your "current solution", so yes, that's what I would suggest.

Is this a redux middleware anti-pattern? How to properly build async actions with middleware

Just built my first API Middleware and was just wondering where I'm suppose to chain promises for action creators that dispatch multiple actions. Is what I did an anti-pattern:
export const fetchChuck = () => {
return {
[CALL_API]: {
types: [ CHUCK_REQUEST, CHUCK_SUCCESS, CHUCK_FAILURE ],
endpoint: `jokes/random`
}
}
}
export const saveJoke = (joke) => {
return { type: SAVE_JOKE, joke: joke }
}
export const fetchAndSaveJoke = () => {
return dispatch => {
dispatch(fetchChuck()).then((response) => {
dispatch(saveJoke(response.response.value.joke))
})
}
}
Should fetchAndSaveJoke dispatch the section action in my react component or is it okay to have it as its own action creator?
I would say that at this point in the Redux world, it's not super clear what's best practice and what the anti-patterns are. It's a very unopinionated tool. While that's been great for a diverse ecosystem to flourish, it does present challenges for people looking for ways to organize their apps without running into pitfalls or excessive boilerplate. From what I can tell, your approach seems to be roughly in line with the advice from the Redux guide. The one thing that looks funny to me is that it seems like CHUCK_SUCCESS should probably make SAVE_JOKE unnecessary.
I personally find it rather awkward to have action creators dispatch more actions, and so I worked out the approach behind react-redux-controller. It's brand new, so it's certainly not a "best practice", but I'll throw it out there in case you or someone else wants to give it a try. In that workflow, you'd have a controller method that looks something like:
// actions/index.js
export const CHUCK_REQUEST = 'CHUCK_REQUEST';
export const CHUCK_SUCCESS = 'CHUCK_SUCCESS';
export const CHUCK_FAILURE = 'CHUCK_FAILURE';
export const chuckRequest = () => { type: CHUCK_REQUEST };
export const chuckSuccess = (joke) => { type: CHUCK_SUCCESS, joke };
export const chuckFailure = (err) => { type: CHUCK_FAILURE, err };
// controllers/index.js
import fetch from 'isomorphic-fetch'; // or whatever
import * as actions from '../actions';
const controllerGenerators = {
// ... other controller methods
*fetchAndSaveJoke() {
const { dispatch } = yield getProps;
// Trigger a reducer to set a loading state in your store, which the UI can key off of
dispatch(actions.chuckRequest());
try {
const response = yield fetch('jokes/random');
dispatch(actions.chuckSuccess(response.response.value.joke));
} catch(err) {
dispatch(actions.chuckFailure(err));
}
},
};

Resources