I'm using the createAsyncThunk method to handle an api request with RTK.
However, I can't get to pass extra arguments to the fulfilled response of the thunk. I can only get the data from the returned promise.
The returned promise has this data:
{ items: [ [Object], [Object] ], metadata: {} }
The action:
export const getGroupsBySchoolId = createAsyncThunk(
'groups/getGroupsBySchoolId',
async (schoolId, _thunkAPI) => {
const { items } = await fetch(someUrl); // simplified fetch request
return { items, schoolId }; // this won't work in the reducer, only if I unwrap() the promise in the component
},
);
in the slice the builder I'm trying to get the schoolId, but I only get the returned promise.
builder.addCase(getGroupsBySchoolId.fulfilled, (state, action) => {
// console.log(action);
const schoolId = action.payload.items.length > 0 ? action.payload.items[0].parentId : null; // i want to avoid this an get it from the payload
state.items[schoolId] = action.payload.items;
state.loading = false;
});
The output from console.loging the action, which is of course, the returned promise and the action type:
{
type: 'groups/getGroupsBySchoolId/fulfilled',
payload: { items: [ [Object], [Object] ], metadata: {} }
}
I could create a regular reducer and dispatch it once the promise has been resolved, but that sounds like an overkill that -I think- shoul be solved in the fulfilled builder callback.
Based on your last comment, I see what you're asking - you want to know how to get access to the thunk argument in the reducer.
In other words, given this:
dispatch(getGroupsBySchoolId(123))
You want to to be able to see the value 123 somewhere in the action when it gets to the reducer.
The good news is this is easy! For createAsyncThunk specifically, the thunk argument will always be available as action.meta.arg. So, this should work:
builder.addCase(getGroupsBySchoolId.fulfilled, (state, action) => {
// console.log(action);
const schoolId = action.meta.arg;
state.items[schoolId] = action.payload.items;
state.loading = false;
});
Related
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;
}
}
})
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.
I'm trying "redux-promise".
When there's no error in the flow, my code works properly. But, let's say that the API is down or I have a typo in the URL. In those cases, I expect to handle the error in the proper way.
This is the API: https://jsonplaceholder.typicode.com/users
(in the snippet I'm adding random text at the end to produce the 404)
Action creator
export async function fetchUsers() {
const request = await axios
.get('https://jsonplaceholder.typicode.com/userssdfdsfdsf')
.catch(error => console.log('ERROR', error))
return {
type: FETCHING_USERS,
payload: request
};
}
Reducer
export default (state = [], action) => {
switch (action.type) {
case FETCHING_USERS:
return [...state, ...action.payload.data]
default:
return state
}
}
I can see the error logged in the console
ERROR Error: Request failed with status code 404
But, once the action is dispatched its payload is undefined
action {type: "FETCHING_USERS", payload: undefined}
I don't know where is the best place to handle this: action creator, reducer, etc. I shouldn't check if payload is something in the reducer and, if not, return state or do nothing. I want to understand which would be the best approach to handle this.
You may look at source of redux-promise, as it very simple.
Redux-promise expects either promise or action with payload set to some promise. I think you're going to use leter case.
Code may look like (just example, not tested):
export function fetchUsers() {
const request = axios.get('https://jsonplaceholder.typicode.com/userssdfdsfdsf');
return {
type: FETCHING_USERS,
payload: request
};
}
In this case redux-promise will await for resolution of promise returned by axios.get and dispatch your action but payload replaced with promise result. In case of error, redux-promise will catch it and dispatch action with error = true (you may want to handle action.error === true case in reducer)
In the reducer you should check for the existence of the error field in action:
export default function(state = null, action) {
if (action.error) {
//handle
}
switch(action.type) {
This code is for the action.
export const fetchUsers = () => async dispatch => {
try {
const res = await axios.get('https://jsonplaceholder.typicode.com/userssdfdsfdsf');
dispatch({
type: FETCHING_USERS,
payload: res.data
});
} catch (err) {
dispatch({
type: FETCHING_ERROR
});
}
};
In reducer do this.
Define the initial state and use this code.
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case FETCHING_USERS:
return {
...state,
loading: false,
user: payload,
};
case FETCH_ERROR:
return {
...state,
token: null,
loading: false,
};
I'm refactoring my react/redux app to use redux-observable instead of redux-thunk. Using thunk, I have an api middleware set up to listen for any actions with a CALL_API key and do some manipulation of the data, prepare headers, prepare full url, perform an api call using axios, and also do some additional action dispatches related to an api call.
Importantly, the api middleware dispatches a REQUEST_START action which gives the request an id and sets its status to pending in the network part of my state. When the promise from axios resolves or rejects, the middleware dispatches a REQUEST_END action, updating the state so that the current request is set to resolved or rejected. Then the response is returned to the calling action creator that initially dispatched the CALL_API action.
I have not been able to figure out how to do this with redux-observable. The part about the api middleware described above that I want to replicate is the REQUEST_START and REQUEST_END action dispatches. It's very convenient to have a centralized place where all api call related stuff is handled. I know I can effectively dispatch the REQUEST_START and REQUEST_END actions in each of my epics that does an api call, but I don't want to have to repeat the same code in many places.
I managed to partially solve this by creating an apiCallEpic which listens for actions with type CALL_API and does the above setup for api calls. However, an issue (or rather, something I don't like) is that the epic that initiates the api call (e.g. getCurrentUserEpic) essentially gives up control to apiCallEpic.
So, for example, when the api call succeeds and has a response, I may want to format that response data in some way before dispatching an action to be handled by my reducer. That is, getCurrentUserEpic should do some formatting of data returned from api call before sending to reducer. I was able to achieve something close to this by passing a payloadHandler callback function defined in getCurrentUserEpic that the apiCallEpic can call if/when it gets a successful response. However, I don't like this callback architecture and it seems like there's got to be a better way.
Here is some code that demonstrates my use of api middleware using thunk.
import axios from 'axios';
// actionCreators.js
// action types
const CALL_API = "CALL_API";
const FETCH_CURRENT_USER = "FETCH_CURRENT_USER";
const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER";
// action creators for request start and end
export const reqStart = (params = {}) => (dispatch) => {
const reduxAction = {
type: REQ_START,
status: 'pending',
statusCode: null,
requestId: params.requestId,
}
dispatch(reduxAction);
}
export const reqEnd = (params = {}) => (dispatch) => {
const {
requestId,
response = null,
error = null,
} = params;
let reduxAction = {}
if (response) {
reduxAction = {
type: REQ_END,
status: 'success',
statusCode: response.status,
requestId,
}
}
else if (error) {
if (error.response) {
reduxAction = {
type: REQ_END,
status: 'failed',
statusCode: error.response.status,
requestId,
}
}
else {
reduxAction = {
type: REQ_END,
status: 'failed',
statusCode: 500,
requestId,
}
}
}
dispatch(reduxAction);
}
// some api call to fetch data
export const fetchCurrentUser = (params = {}) => (dispatch) => {
const config = {
url: '/current_user',
method: 'get',
}
const apiCall = {
[CALL_API]: {
config,
requestId: FETCH_CURRENT_USER,
}
}
return dispatch(apiCall)
.then(response => {
dispatch({
type: RECEIVE_CURRENT_USER,
payload: {response},
})
return Promise.resolve({response});
})
.catch(error => {
return Promise.reject({error});
})
}
// apiMiddleware.js
// api endpoint
const API_ENTRY = "https://my-api.com";
// utility functions for request preparation
export const makeFullUrl = (params) => {
// ...prepend endpoint url with API_ENTRY constant
return fullUrl
}
export const makeHeaders = (params) => {
// ...add auth token to headers, etc.
return headers;
}
export default store => next => action => {
const call = action[CALL_API];
if (call === undefined) {
return next(action);
}
const requestId = call.requestId;
store.dispatch(reqStart({requestId}));
const config = {
...call.config,
url: makeFullUrl(call.config),
headers: makeHeaders(call.config);
}
return axios(config)
.then(response => {
store.dispatch(reqEnd({
response,
requestId,
}))
return Promise.resolve(response);
})
.catch(error => {
store.dispatch(reqEnd({
error,
requestId,
}))
return Promise.reject(error);
})
}
// reducers.js
// Not included, but you can imagine reducers handle the
// above defined action types and update the state
// accordingly. Most usefully, components can always
// subscribe to specific api calls and check the request
// status. Showing loading indicators is one
// use case.
Here's the code I've implemented to accomplish a similar thing with redux-observable.
export const fetchCurrentUserEpic = (action$, state$) => {
const requestType = FETCH_CURRENT_USER;
const successType = RECEIVE_CURRENT_USER;
const requestConfig = {
url: "/current_user",
method: "get",
}
const payload = {requestConfig, requestType, successType};
const payloadNormalizer = ({response}) => {
return {currentUser: response.data.data};
}
return action$.ofType(FETCH_CURRENT_USER).pipe(
switchMap((action) => of({
type: CALL_API,
payload: {...payload, requestId: action.requestId, shouldFail: action.shouldFail, payloadNormalizer},
})),
)
}
export const apiEpic = (action$, state$) => {
return action$.ofType(CALL_API).pipe(
mergeMap((action) => (
concat(
of({type: REQ_START, payload: {requestId: action.payload.requestId, requestType: action.payload.requestType}}),
from(callApi(action.payload.requestConfig, action.payload.shouldFail)).pipe(
map(response => {
return {
type: action.payload.successType,
payload: action.payload.payloadNormalizer({response})
}
}),
map(() => {
return {
type: REQ_END,
payload: {status: 'success', requestId: action.payload.requestId, requestType: action.payload.requestType},
}
})
)
)
).pipe(
catchError(error => {
console.log('error', error);
return of({type: REQ_END, payload: {status: 'failed', requestId: action.payload.requestId, requestType: action.payload.requestType}, error});
})
)
)
)
}
Any comments or suggestions are appreciated!
I've found redux-fetch-epic-builder (A lib for building "fetch actions" and generic epics handled by redux-observable) to be similar to what you are trying to achieve here (beware it uses rxjs 5, this guide to rescue). It uses fetch, not axios, but it's easy to replace that. Plus it has transformers for successful/failed actions.
The library is a bit old, but the base idea to overcome boilerplate code is still valid: Generic epic-builder to fetch data with calls to API(s).
I am a novice in React / Redux / RxJS, but the only problem I see with the redux-fetch-epic-builder is the way to configure the client (in axios terms). That is, I am not fully satisfied with (due to it being not FSA or RSAA):
//action creators
const getComments = (id, page = 1) => ({
type: GET_COMMENTS,
host: 'http://myblog.com',
path: `/posts/${id}/comments`,
query: {
page,
},
})
// ...
const epics = [
buildEpic(GET_COMMENTS),
]
but this may still be an elegant way. And the license allow to develop the library further. I have not converted the example from the library documentation to your user-related example, but with react-observable there is certainly no need to introduce a separate "api middleware". (Also, I like /SUBACTION better than _SUBACTION, but it's trivial to change.)
I'm dispatching an ADD_SOURCE action from my component that when it success dispatches another ADD_SOURCE_SUCCESS:
this.store$
.select(fromRoot.getUserState)
.filter(user => user.id != null && user.logged)
.takeUntil(this.componentDestroyed$)
.do(user => this.store$.dispatch({type: 'ADD_SOURCE', payload: user.username}))
.subscribe();
This is the effect that returns the ADD_SOURCE_SUCCESS according to a net call:
#Effect({ dispatch: true })
addSource$: Observable<Action> = this.actions$
.ofType('ADD_SOURCE')
.switchMap(
(action: Action) =>
this.userService.addCard(action.payload.username, action.payload.token)
.map((card: CardDTO) => {
return <Action>{
type: 'ADD_SOURCE_SUCCESS',
payload: <ICard>{ ... }
};
})
.catch(_ => {
return Observable.of(<Action>{ type: 'ADD_SOURCE_FAILED', payload: { }});
}));
So, then a new ADD_SOURCE_SUCCESS is dispatched on my reducer:
private static saveSourceSuccess(sourcesRdx, type, payload) {
return <ISourceRedux>{
ids: [ ...sourcesRdx.ids, payload.id ],
entities: Object.assign({}, sourcesRdx.entities, {[payload.id]: payload}),
selectedIds: sourcesRdx.selectedIds,
editingSource: null
};
}
Nevertheless, I don't quite figure out how to say on my component that the operation has been success and do one thing or another one...
Any ideas?
in your reducer, you should also handle the success & fail errors, set the store with an error message or whatever you need as data, and make a selector on it. Then the ui should subscribe to this new selector and you ll be notified
check this chapter
selectors