How to pass parameter from thunk to extraReducers? - redux

I know extraReducers can receive payload data, but can it receive patameter directly from thunk method?
Normally I have this in extraReducers:
.addCase(loginEmail.fulfilled, (state, { payload }) => {
state.authnRes = payload
})
But I would get data from thunk itself, how can I pass it?
export const logout = createAsyncThunk(`${namespace}/logout`, async () => {
const { data } = await axios({
method: 'post',
url: 'logout',
headers: { crossDomain: true },
})
return data
})

By returning it - your return data already does it.
Whatever you return from the thunk will end up as action.payload in that reducer.

Related

Handling errors with redux-toolkit

The information about the error in my case sits deeply in the response, and I'm trying to move my project to redux-toolkit. This is how it used to be:
catch(e) {
let warning
switch (e.response.data.error.message) {
...
}
}
The problem is that redux-toolkit doesn't put that data in the rejected action creator and I have no access to the error message, it puts his message instead of the initial one:
While the original response looks like this:
So how can I retrieve that data?
Per the docs, RTK's createAsyncThunk has default handling for errors - it dispatches a serialized version of the Error instance as action.error.
If you need to customize what goes into the rejected action, it's up to you to catch the initial error yourself, and use rejectWithValue() to decide what goes into the action:
const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
if (!err.response) {
throw err
}
return rejectWithValue(err.response.data)
}
}
)
We use thunkAPI, the second argument in the payloadCreator; containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options: For our example async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) is our payloadCreator with the required arguments;
This is an example using fetch api
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const getExampleThunk = createAsyncThunk(
'auth/getExampleThunk',
async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) => {
try{
const response = await fetch('https://reqrefs.in/api/users/yu');
if (!response.ok) {
return rejectWithValue(response.status)
}
const data = await response.json();
return fulfillWithValue(data)
}catch(error){
throw rejectWithValue(error.message)
}
}
)
Simple example in slice:
const exampleSlice = createSlice({
name: 'example',
initialState: {
httpErr: false,
},
reducers: {
//set your reducers
},
extraReducers: {
[getExampleThunk.pending]: (state, action) => {
//some action here
},
[getExampleThunk.fulfilled]: (state, action) => {
state.httpErr = action.payload;
},
[getExampleThunk.rejected]: (state, action) => {
state.httpErr = action.payload;
}
}
})
Handling Error
Take note:
rejectWithValue - utility (additional option from thunkAPI) that you can return/throw in your action creator to return a rejected response with a defined payload and meta. It will pass whatever value you give it and return it in the payload of the rejected action.
For those that use apisauce (wrapper that uses axios with standardized errors + request/response transforms)
Since apisauce always resolves Promises, you can check !response.ok and handle it with rejectWithValue. (Notice the ! since we want to check if the request is not ok)
export const login = createAsyncThunk(
"auth/login",
async (credentials, { rejectWithValue }) => {
const response = await authAPI.signin(credentials);
if (!response.ok) {
return rejectWithValue(response.data.message);
}
return response.data;
}
);

Redux request statuses. Reduce boilerplate

I am using Redux with react and redux-thunk as a middleware.
When I make http requests I have to dispatch three actions in my thunks.
I will use my auth example.
here are my actions:
export const loginSuccess = () => ({
type: AUTH_LOGIN_SUCCESS,
})
export const loginFailure = (errorMessage) => ({
type: AUTH_LOGIN_FAILURE,
errorMessage,
})
export const loginRequest = () => ({
type: AUTH_LOGIN_REQUEST,
})
and here is the thunk which combines above three actions:
export const login = (credentials) => dispatch => {
dispatch(loginRequest())
const options = {
method: 'post',
url: `${ENDPOINT_LOGIN}?username=${credentials.username}&password=${credentials.password}`,
}
axiosInstance(options)
.then(response => {
dispatch(loginSuccess())
dispatch(loadUser(response.data)) // I have separate action for user and separate reducer.
window.localStorage.setItem(ACCESS_TOKEN_KEY, response.data.token)
})
.catch(error => {
return dispatch(loginFailure(error))
})
}
And here is my reducer:
const initialState = {
pending: false,
error: false,
errorMessage: null,
}
export const loginReducer = (state = initialState, action) => {
switch (action.type) {
case AUTH_LOGIN_SUCCESS:
return {
...state,
pending: false,
error: false,
errorMessage: null,
}
case AUTH_LOGIN_FAILURE:
const { errorMessage } = action
return {
...state,
pending: false,
error: true,
errorMessage,
}
case AUTH_LOGIN_REQUEST:
return {
...state,
pending: true,
}
default:
return state
}
}
I have to do almost exact same things when I am sending another request, for example in case of logout. I feel like I am repeating myself a lot and there must be a better way.
I need to know what is the best practice to handle this issue.
Any other corrections and recommendations will be appreciated.
If you are looking for "ready to use" solution take a look at:
https://redux-toolkit.js.org/api/createAsyncThunk
https://redux-resource.js.org/ (but it is written with js (not TS), and no #types definition for this library)
If you are looking for a custom solution you can create a few factories:
factory for reducer
factory for three actions
factory for thunk
const actions = createActions('My request name');
const reducer = createReducer(actions);
...
const thunk = createThunk(config);
or even you can combine them:
const { actions, reducer, thunk } = createRequestState('Name...', config);
... but this is just an idea.

Proper way to handle errors in redux using redux-promise

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

Api middleware with redux-observable

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.)

Redux Async actions returns me an error: Actions must be plain objects. Use custom middleware for async actions

I am struggling with the async Redux (thunk). I trully don't understand what I am doing wrong with my async actions and why I get the error : Error: Actions must be plain objects. Use custom middleware for async actions.
export async function startLocalizationFetchingAsync(currentLocalizationState) {
return (dispatch) => {
let payload = {
request: {
sent:true,
}
};
dispatch({
type: "NEW_LOCALIZATION_REQUEST_SENT2",
payload: payload,
});
return axios.get("http://freegeoip.net/json/"+currentLocalizationState.clientIP)
.then(res => {
res = res.data;
var payload = {
country: res.country_name||'',
};
dispatch({
type: "NEW_LOCALIZATION",
payload: payload,
});
})
.catch(function (error) {
console.log("Promise Rejected",error);
dispatch({
type: "NEW_LOCALIZATION_REQUEST_ERROR",
payload: null,
});
});
};
}
while in the index.js router i have the following code
async action({ next, store }) {
// Execute each child route until one of them return the result
const route = await next();
await store.dispatch(startLocalizationFetchingAsync());
this generates me an error:
Error: Actions must be plain objects. Use custom middleware for async actions.
dispatch
webpack:///~/redux/es/createStore.js:153
http://myskyhub.ddns.net:3000/assets/client.js:9796:16
http://myskyhub.ddns.net:3000/assets/vendor.js:46309:16
Object.dispatch
webpack:///~/redux-thunk/lib/index.js:14
Object._callee$
webpack:///src/routes/index.js?a731:35
tryCatch
webpack:///~/regenerator-runtime/runtime.js:65
Generator.invoke
webpack:///~/regenerator-runtime/runtime.js:303
Generator.prototype.(anonymous
webpack:///~/regenerator-runtime/runtime.js:117
http://myskyhub.ddns.net:3000/assets/3.9645f2aeaa83c71f5539.hot-update.js:8:361
while the config store is the following
const middleware = [thunk.withExtraArgument(helpers), thunk.withExtraArgument(AsyncMiddleware)];
let enhancer;
if (__DEV__) {
middleware.push(createLogger());
//middleware.push(AsyncMiddleware());
enhancer = compose(
applyMiddleware(...middleware),
devToolsExtension,
);
} else {
enhancer = applyMiddleware(...middleware);
}
initialState.localization = defaultLocalization; //Location
// See https://github.com/rackt/redux/releases/tag/v3.1.0
const store = createStore(rootReducer, initialState, enhancer);
What I am doing wrong? I don't understand the redux-thunk...

Resources