createAsyncThunk: abort previous request - javascript

I'm using createAsyncThunk to make asynchronous requests to some API. Only one request should be active at any given moment.
I understand that the request can be aborted using a provided AbortSignal if I have the Promise returned from the previous invocation. Question is, can the thunk itself somehow abort the previous request "autonomously"?
I was considering two options:
keeping the last AbortSignal in the state. Seems wrong, because state should be serializable.
keeping the last Promise and its AbortSignal in global variable. Seems also wrong, because, you know, global variables.
Any ideas? Thank you.

I don't know how your specific api works but below is a working example of how you can put the abort logic in the action and reducer, it will abort any previously active fake fetch when a newer fetch is made:
import * as React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose,
} from 'redux';
import {
Provider,
useDispatch,
useSelector,
} from 'react-redux';
import {
createAsyncThunk,
createSlice,
} from '#reduxjs/toolkit';
const initialState = {
entities: [],
};
// constant value to reject with if aborted
const ABORT = 'ABORT';
// fake signal constructor
function Signal() {
this.listener = () => undefined;
this.abort = function () {
this.listener();
};
}
const fakeFetch = (signal, result, time) =>
new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve(result), time);
signal.listener = () => {
clearTimeout(timer);
reject(ABORT);
};
});
// will abort previous active request if there is one
const latest = (fn) => {
let previous = false;
return (signal, result, time) => {
if (previous) {
previous.abort();
}
previous = signal;
return fn(signal, result, time).finally(() => {
//reset previous
previous = false;
});
};
};
// fake fetch that will abort previous active is there is one
const latestFakeFetch = latest(fakeFetch);
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async ({ id, time }) => {
const response = await latestFakeFetch(
new Signal(),
id,
time
);
return response;
}
);
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {},
extraReducers: {
[fetchUserById.fulfilled]: (state, action) => {
state.entities.push(action.payload);
},
[fetchUserById.rejected]: (state, action) => {
if (action?.error?.message === ABORT) {
//do nothing
}
},
},
});
const reducer = usersSlice.reducer;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
({ dispatch, getState }) => (next) => (action) =>
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const App = () => {
const dispatch = useDispatch();
React.useEffect(() => {
//this will be aborted as soon as the next request is made
dispatch(
fetchUserById({ id: 'will abort', time: 200 })
);
dispatch(fetchUserById({ id: 'ok', time: 100 }));
}, [dispatch]);
return 'hello';
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
If you only need to resolve a promise if it was the latest requested promise and no need to abort or cancel ongoing promises (ignore resolve if it wasn't latest) then you can do the following:
const REPLACED_BY_NEWER = 'REPLACED_BY_NEWER';
const resolveLatest = (fn) => {
const shared = {};
return (...args) => {
//set shared.current to a unique object reference
const current = {};
shared.current = current;
fn(...args).then((resolve) => {
//see if object reference has changed
// if so it was replaced by a newer one
if (shared.current !== current) {
return Promise.reject(REPLACED_BY_NEWER);
}
return resolve;
});
};
};
How it's used is demonstrated in this answer

Based on #HMR answer, I was able to put this together, but it's quite complicated.
The following function creates "internal" async thunk that does the real job, and "outer" async thunk that delegates to the internal one and aborts previous dispatches, if any.
The payload creator of the internal thunk is also wrapped to: 1) wait for the previous invocation of payload creator to finish, 2) skip calling the real payload creator (and thus the API call) if the action was aborted while waiting.
import { createAsyncThunk, AsyncThunk, AsyncThunkPayloadCreator, unwrapResult } from '#reduxjs/toolkit';
export function createNonConcurrentAsyncThunk<Returned, ThunkArg>(
typePrefix: string,
payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg>,
options?: Parameters<typeof createAsyncThunk>[2]
): AsyncThunk<Returned, ThunkArg, unknown> {
let pending: {
payloadPromise?: Promise<unknown>;
actionAbort?: () => void;
} = {};
const wrappedPayloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg> = (arg, thunkAPI) => {
const run = () => {
if (thunkAPI.signal.aborted) {
return thunkAPI.rejectWithValue({name: 'AbortError', message: 'Aborted'});
}
const promise = Promise.resolve(payloadCreator(arg, thunkAPI)).finally(() => {
if (pending.payloadPromise === promise) {
pending.payloadPromise = null;
}
});
return pending.payloadPromise = promise;
}
if (pending.payloadPromise) {
return pending.payloadPromise = pending.payloadPromise.then(run, run); // don't use finally(), replace result
} else {
return run();
}
};
const internalThunk = createAsyncThunk(typePrefix + '-protected', wrappedPayloadCreator);
return createAsyncThunk<Returned, ThunkArg>(
typePrefix,
async (arg, thunkAPI) => {
if (pending.actionAbort) {
pending.actionAbort();
}
const internalPromise = thunkAPI.dispatch(internalThunk(arg));
const abort = internalPromise.abort;
pending.actionAbort = abort;
return internalPromise
.then(unwrapResult)
.finally(() => {
if (pending.actionAbort === abort) {
pending.actionAbort = null;
}
});
},
options
);
}

Related

What is the reason getState() not functioning in React-Redux?

export const SMSAdmin_Filter_User = (userName) => (dispatch, getState) => {
var st = getState();
...
}
When this code runs, getState() is defined in the debugger as a function, but st comes up as undefined. I have used getState in multiple other action creator functions with great success, so am uncertain why it is not functioning here.
This function is called as a promise since there are other asynchronous processes running (incremental fetch for large number of users).
Here is where it is being called from:
componentDidMount() {
var el = document.getElementById("userList");
if (el) {
el.focus({ preventScroll: true });
}
// console.log("SMSAdmin")
new Promise((res,rej)=>this.props.SMSAdmin_Get_Users());
// .then(() => {
// this.setState({ users: this.props.SMSAdmin.users });
// });
}
filterUsers = () => {
let target = document.getElementById("userName");
let name = target?.value?.toLowerCase();
new Promise((res, rej)=>SMSAdmin_Filter_User(name?.trim()));
}
filterUsers() is also being called from the render function to ensure updates when SMSAdmin_Get_Users() adds more users to the list.
Here is the mapDispatchToProps():
const mapDispatchToProps = (dispatch) => {
return {
SMSAdmin_Get_Users: () => { return dispatch(SMSAdmin_Get_Users()) },
SMSAdmin_Load_User: (userId, userName, oldData = null, startVal = 0, number = 20) => {
return dispatch(SMSAdmin_Load_User(userId, userName, oldData, startVal, number))
},
SMSAdmin_Update_User: (user, province, credits) => { return dispatch(SMSAdmin_Update_User(user, province, credits)) },
SMSAdmin_setCurrentUpload: (userName) => { return dispatch(SMSAdmin_setCurrentUpload(userName)) },
SMSAdmin_Filter_User: (userName) => { return dispatch(SMSAdmin_Filter_User(userName)) },
}
}
I am not able to provide a sandbox for the code because there are multiple other files associated with this component and the data being used is confidential.
Thanks.
Edit: Showing redux store creation
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { reducers } from './reducerMain.js';
export const ConfigureStore = () => {
const store = createStore(
reducers,
applyMiddleware(thunk, logger)
);
return store;
}
I guess you are accidently using imported function (not the one mapped in mapDispatchToProps). Did you forgot to use the one from props ? like that:
filterUsers = () => {
// ...
new Promise((res, rej)=>this.props.SMSAdmin_Filter_User(name?.trim()));
}

How to make a refetch wait for a POST in React-Query

There are two requests, a POST and a GET. The POST request should create data and after it has created that data, the GET request should fetch the newly created data and show it somewhere.
This are the hooks imported into the component:
const { mutate: postTrigger } = usePostTrigger();
const { refetch } = useGetTriggers();
And they are used inside an onSubmit method:
const onAddSubmit = async (data) => {
await postTrigger(data);
toggle(); // this one and the one bellow aren't important for this issue
reset(emptyInput); //
refetch();
};
Tried to add async / await in order to make it wait until the POST is finished but it doesn't.
Any suggestions?
I added here the code of those 2 hooks if it's useful:
POST hook:
import { useMutation } from 'react-query';
import { ICalculationEngine } from '../constants/types';
import calculationEngineAPI from '../services/calculation-engine-api';
export const usePostTrigger = () => {
const apiService = calculationEngineAPI<ICalculationEngine['TriggerDailyOpt1']>();
const mutation = useMutation((formData: ICalculationEngine['TriggerDailyOpt1']) =>
apiService.post('/trigger/DailyOpt1', formData)
);
return {
...mutation
};
};
export default usePostTrigger;
GET hook:
import { useMemo } from 'react';
import { useInfiniteQuery } from 'react-query';
import { ICalculationEngine } from '../constants/types';
import { calculationEngineAPI } from '../services/calculation-engine-api';
export const TAG_PAGE_SIZE = 20;
export interface PaginatedData<D> {
totalPages: number;
totalElements: number;
content: D[];
}
export const useGetTriggers = () => {
const query = 'getTriggers';
const apiService = calculationEngineAPI<PaginatedData<ICalculationEngine['Trigger']>>();
const fetchTriggers = (pageNumber: number) => {
const search = {
pageNumber: pageNumber.toString(),
pageSize: TAG_PAGE_SIZE.toString()
};
return apiService.get(`/trigger/paged/0/${search.pageSize}`);
};
const {
data: response,
isError,
isLoading,
isSuccess,
isFetching,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
refetch,
...rest
} = useInfiniteQuery(query, ({ pageParam = 1 }) => fetchTriggers(pageParam), {
getNextPageParam: (lastPage, pages) => {
const totalPages = lastPage.data.totalPages || 1;
return totalPages === pages.length ? undefined : pages.length + 1;
}
});
const data = useMemo(
() => response?.pages.map((page) => page.data.content).flat() || [],
[response?.pages]
);
return {
data,
isError,
isLoading,
isSuccess,
isFetching,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
refetch,
...rest
};
};
export default useGetTriggers;
You can use the onSuccess method of react-query (https://react-query.tanstack.com/reference/useMutation)
onSuccess: (data: TData, variables: TVariables, context?: TContext) => Promise | void
Optional
This function will fire when the mutation is successful and will be passed the mutation's result.
If a promise is returned, it will be awaited and resolved before proceeding
const { mutate, isLoading, error, isSuccess } = useMutation(
(formData: ICalculationEngine['TriggerDailyOpt1']) =>
apiService.post('/trigger/DailyOpt1', formData),
{
mutationKey: 'DailyOpt1',
onSuccess: (_, { variables }) => {
// Execute your query as you see fit.
apiService.get(...);
},
}
);
As a best practice thought I would suggest the POST request to return the updated data if possible to avoid this exact need.

Passing params from one Axios request to another

Background
I'm connecting an app built in React Native to a REST API. I'm handling requests via Axios and storing the results from queries with Redux. I have an index.js file for my api connections which holds the functions that act as handlers for requests which require deeper and deeper levels of authorization. I have a simple function which returns an access token, this is triggered by the following code which currenty is located in the app's "Welcome page".
useEffect(() => {
dispatch(retrieveToken());
}, [dispatch]);
Ideally, after navigating through a couple of screens, the user would get to the Home Page and trigger the following code:
useEffect(() => {
dispatch(retrieveData());
}, [dispatch]);
So far, so good. These are the functions which dispatch triggers:
export const getToken = () =>
apiInstance
.request({
url: ENDPOINTS.TOKEN,
data: qs.stringify({
grant_type: 'some_credentials',
c_id: 'some_id',
c_secret: 'some_secret',
}),
headers: {
'content-type': 'some_content_type',
},
method: 'POST',
})
.then(response => {
return response.data;
})
.catch(error => {
return Promise.reject(error.message);
});
export const getData = () =>
apiInstance
.request({
url: ENDPOINTS.DATA,
method: 'POST',
data: qs.stringify({
timestamp: Date.now(),
c_id: 'some_id',
token: **this is the token we get from the previous function**,
}),
headers: {
'content-type': 'some_content_type',
},
})
.then(response => {
return response.data;
})
.catch(error => {
return Promise.reject(error.message);
});
Problem
As I mentioned before, this is a Redux/Axios solution. This means state is stored globally but there is an order of execution. You should note that these two functions are stored within the same file and are not called upon unless explicitly stated such as with the two dispatch calls I've showed before.
Thing is, if I log the token from Home (after calling it with dispatch) I can see it clearly, however if I try to log said token from the file which stores the request functions, I get an empty array. I've tried to fill the token field in all the following ways:
const state = store.getState()
token: state.token
const getData = (Token) =>{
...
token: Token}
And passing Token as a param within dispatch.
I've also tried daisy-chaining the different dispatches in order to force the execution of
getData after retrieving the token and not before.
Question
How can I access the result of an axios query from within another, in specific order?
It is very important to note that the data in the API can only be accessed via POST and that the error code I get when executing getData() is 401, incorrect credentials.
Also, remember this is a Redux application. The results of the queries are stored withing a global state. However this state cannot be accessed from outside components, and I cannot call it from the file in which the queries are executed given the token "does not exist at that point in time."
Action code
import keyMirror from 'keymirror';
import {createAction} from 'redux-actions';
import {getToken} from '../../api';
export const tokenActionTypes = keyMirror({
RETRIEVE_TOKEN_REQUEST: null,
RETRIEVE_TOKEN_SUCCESS: null,
RETRIEVE_TOKEN_FAILURE: null,
});
const tokenActionCreators = {
request: createAction(tokenActionTypes.RETRIEVE_TOKEN_REQUEST),
success: createAction(tokenActionTypes.RETRIEVE_TOKEN_SUCCESS),
failure: createAction(tokenActionTypes.RETRIEVE_TOKEN_FAILURE),
};
export const retrieveToken = () => dispatch => {
dispatch(tokenActionCreators.request());
getToken()
.then(token => dispatch(tokenActionCreators.success(token)))
.catch(error => dispatch(tokenActionCreators.failure(error)));
};
Reducer code
import {tokenActionTypes} from '../actions/token';
export const initialState = {
loadingToken: false,
token: [],
error: null,
};
const actionsMap = {
[tokenActionTypes.RETRIEVE_TOKEN_REQUEST]: state => ({
...state,
loadingToken: true,
}),
[tokenActionTypes.RETRIEVE_TOKEN_SUCCESS]: (state, action) => ({
...state,
loadingToken: false,
token: action.payload,
}),
[tokenActionTypes.RETRIEVE_TOKEN_FAILURE]: (state, action) => ({
...state,
loadingToken: false,
error: action.payload,
}),
};
export default (state = initialState, action) => {
const actionHandler = actionsMap[action.type];
if (!actionHandler) {
return state;
}
return actionHandler(state, action);
};
You could combine one thunk in another, like combining get token in get data:
export const retrieveToken = () => (dispatch, getState) => {
//you could use getState() to see if you need to fetch the token
// const tokenResult = selectToken(getState());
// if(token && !token expired) { return Promise.resolve() }
dispatch(tokenActionCreators.request());
//return a promise so you can wait for it
return getToken()
.then(token => dispatch(tokenActionCreators.success(token)))
.catch(error => dispatch(tokenActionCreators.failure(error)));
};
//in retrieve data you can wait for the token
export const retrieveData = () => dispatch => {
dispatch(retrieveToken()).then(
()=>{
//here return getting the data
}
)
};
A possible bug in that code is that one render cycle will dispatch multiple thunks that will get the token multiple times. You can solve that by grouping the retrieveToken action with a cache that invalidates on resolve:
const invalidateOnResolveCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (x) => cache.delete(key),
};
};
Or you can write a wrap function for all thunks that need a token:
//group retrieveToken in such a way that if it's called multiple times
// during a render cycle the token request will only be made once
//https://gist.github.com/amsterdamharu/2dde4a6f531251f3769206ee44458af7
export const needsToken =
(fn) =>
(...args) =>
(dispatch, getState) =>
dispatch(retrieveToken(...args)).then(() =>
//you could use getState to get the token and pass it to
// fn together with the other args
// for example: fn(...args.concat(selectToken(getState())))
fn(...args)
);
export const autoTokenRetrieveData = needsToken(retrieveData);
//use needsToken for any other thunk actions that need a token
Example:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
//grouping code to group your actions
//group promise returning function
const createGroup =
(cache) =>
(fn, getKey = (...x) => JSON.stringify(x)) =>
(...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//thunk action creators are not (...args)=>result but
// (...args)=>(dispatch,getState)=>result
// so here is how we group thunk actions
const createGroupedThunkAction = (thunkAction, cache) => {
const group = createGroup(cache)(
(args, dispatch, getState) =>
thunkAction.apply(null, args)(dispatch, getState)
);
return (...args) =>
(dispatch, getState) => {
return group(args, dispatch, getState);
};
};
const createInvalidateOnResolveCache = (
cache = new Map()
) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (key) => cache.delete(key),
};
};
//function that fetches token
const uniqueToken = (
(token) => () =>
token++
)(1);
const fetchToken = () => Promise.resolve(uniqueToken());
const initialState = {
data1: [],
data2: [],
token: null,
};
//action types
const DATA_SUCCESS = 'DATA_SUCCESS';
const GOT_TOKEN = 'GOT_TOKEN';
//action creators
const dataSuccess = (data, key) => ({
type: DATA_SUCCESS,
payload: { key, data },
});
const gotToken = (token) => ({
type: GOT_TOKEN,
payload: token,
});
const reducer = (state, { type, payload }) => {
if (type === DATA_SUCCESS) {
const { data, key } = payload;
return {
...state,
[key]: data,
};
}
if (type === GOT_TOKEN) {
return {
...state,
token: {
value: payload,
created: Date.now(),
},
};
}
return state;
};
//thunk getting the data
const getData1 = (token) => (dispatch) =>
Promise.resolve().then(() =>
dispatch(
dataSuccess(
`got data 1 with token: ${token}`,
'data1'
)
)
);
const getData2 = (token) => (dispatch) =>
Promise.resolve().then(() =>
dispatch(
dataSuccess(
`got data 2 with token: ${token}`,
'data2'
)
)
);
//thunk getting the token:
const getToken = () => (dispatch) =>
fetchToken().then((token) => dispatch(gotToken(token)));
//grouped thunk getting token
const getTokenGrouped = createGroupedThunkAction(
getToken,
createInvalidateOnResolveCache()
);
const needsToken =
(fn) =>
(...args) =>
(dispatch, getState) => {
let promise;
//only fetch token if it's older than 1 second
const tokenResult = selectToken(getState());
if (
tokenResult &&
Date.now() - new Date(tokenResult.created).getTime() <
1000
) {
promise = Promise.resolve();
} else {
promise = dispatch(getTokenGrouped(...args));
}
return promise.then(() =>
dispatch(
fn(...args.concat(selectTokenValue(getState())))
)
);
};
const getData1WithToken = needsToken(getData1);
const getData2WithToken = needsToken(getData2);
//selectors
const selectData1 = (state) => state.data1;
const selectData2 = (state) => state.data2;
const selectToken = (state) => state.token;
const selectTokenValue = createSelector(
[selectToken],
//SO snippet has no optional chaining, should just return token?.value
(token) => token && token.value
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
//simple thunk middleware
({ dispatch, getState }) =>
(next) =>
(action) =>
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const Data1 = React.memo(function Data1({ refresh }) {
const data = useSelector(selectData1);
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(getData1WithToken());
}, [dispatch, refresh]);
return <div>{data}</div>;
});
const Data2 = React.memo(function Data2({ refresh }) {
const data = useSelector(selectData2);
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(getData2WithToken());
}, [dispatch, refresh]);
return <div>{data}</div>;
});
const App = () => {
const [refresh, setRefresh] = React.useState({});
return (
<div>
{/* getting data in one render cycle many times */}
<Data1 refresh={refresh} />
<Data2 refresh={refresh} />
<Data1 refresh={refresh} />
<Data2 refresh={refresh} />
<Data1 refresh={refresh} />
<Data2 refresh={refresh} />
<button onClick={() => setRefresh({})}>
refresh
</button>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://unpkg.com/immer#7.0.5/dist/immer.umd.production.min.js"></script>
<div id="root"></div>
Explanation:
Everywhere you see const add export so export const or export default and you can import that from any other file.
The createGroupedThunkAction receives getToken thunk and returns a thunk that is stored in getTokenGrouped.
When getTokenGrouped is called multiple times during one render (Data1 and Data2 have an effect that will do so) it will share getting the token for that render and when it resolves it'll delete the cache because of the type of cache used implemented in createInvalidateOnResolveCache. So no multiple tokens will be fetched during one render no matter how many times you dispatch it during a render.
The needsToken function will receive a thunk (getData1 and getData2) and returns a thunk that will automatically get a token by dispatching getTokenGrouped if there is no current token or if the token is older than one second (my made up logic to invalidate the token). This token is stored in state and passed to getData1 and getData2 so they can use that token.
I suggest opening the redux devtools while running the example so you can see the actions being dispatched. Normally with async you would dispatch beforeFetch, afterFetch or faildFetch for async actions but for simplicity I left that out.
You could try to use createGroupedThunkAction to make a grouped getData1 and getData2 as an exercise so there is no needless fetching for these as well.

My React-Redux Code is Running Functions Before the Previous Functions can Finish

How do I write these functions so that the dispatch finalizes before the if statements start? Currently, the if statements start console.log()'ing before the dispatch can even save the data.
const doStuff = () => {
dispatch(getStuff()); // WAIT FOR ME TO COMPLETE BEFORE MOVING ON!!!
if (stuff) { //stuff is retreived from global state
console.log("stuff available") //I WAITED FOR THE DISPATCH!
}
if (!stuff) { //stuff is retreived from global state
console.log("no stuff") //I WAITED FOR THE DISPATCH!
}
};
export const getStuff = () => { \\WAIT FOR ME TO SAVE STUFF!!!
return (dispatch) => {
axios.get(`/stuff`).then((response) => {
dispatch({
type: SAVE_STUFF_TO_STATE, //stuff is saved to global state
payload: response.data, //stuff
});
})
} catch (err) {
console.log("stuff issue")
}
};
};
You can return the promise from the function, you can try using https://github.com/reduxjs/redux-thunk
enter code hereexport const getStuff = () => { \\WAIT FOR ME TO SAVE STUFF!!!
return (dispatch) => {
axios.get(`/stuff`).then((response) => {
return Promise.resolve(
// the actions to be dispatched goes here
);
})
} catch (err) {
console.log("stuff issue")
}
};
};
and wait for it execution,
const doStuff = () => {
dispatch(getStuff()).then(()=> { // WAIT FOR ME TO COMPLETE BEFORE MOVING ON!!!
if (stuff) { //stuff is retreived from global state
console.log("stuff available") //I WAITED FOR THE DISPATCH!
}
if (!stuff) { //stuff is retreived from global state
console.log("no stuff") //I WAITED FOR THE DISPATCH!
}
});
};
In general, if you want to chain certain activities (as per your example) on the dispatch, your action would need to return a Promise
Your promise creating function needs to return a promise and your thunk action needs to return that promise:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = {
status: 'initial',
};
//action types
const ONE = 'ONE';
const TWO = 'TWO';
//action creators
const later = (value, time = 2000) =>
new Promise((resolve) =>
setTimeout(() => resolve(value), time)
);
const one = () => (dispatch) =>
//return the promise here
later()
.then(() => dispatch({ type: ONE }))
.then(() => later('the value one resolves with'));
const two = () => ({ type: TWO });
const reducer = (state, { type, payload }) => {
if (type === ONE) return { ...state, status: 'one' };
if (type === TWO) return { ...state, status: 'two' };
return state;
};
//selectors
const selectStatus = (state) => state.status;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
({ dispatch, getState }) => (next) => (action) =>
//basic thunk implementation
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const App = () => {
const dispatch = useDispatch();
React.useEffect(() => {
one()(dispatch).then((oneResolve) => {
console.log('one resolved with:', oneResolve);
dispatch(two());
});
}, [dispatch]);
const status = useSelector(selectStatus);
return <h1>Status:{status}</h1>;
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>

How to trap HTTP 4xx errors when using Redux Toolkit async REST API pattern?

I have successfully gotten my React / Redux app to retrieve data from a REST API back end. I'm using the createAsyncThunk feature of Redux Toolkit, which automatically sets up reducers that are called when the HTTP fetch promise resolves with success or failure.
For this particular endpoint, I'd like the Redux store to reflect an error whenever an HTTP 404 Not Found is encountered. Currently that is not happening. The component shown below always returns "Loaded successfully". How can I make it display "Error" instead?
I understand that fetch doesn't resolve with an error on HTTP 4xx errors, and that I need to check the response code myself and resolve it as a failure. What I don't understand is where or how to do that in the code below. I struggle with understanding async/await conceptually, am new to Redux Toolkit, and the code below is already tweaking my brain pretty hard. Help?
Here is my full code:
features/recipeList/recipeListApi.js
export default async function recipeListApi(localApiKey) {
const response = await fetch('https://httpstat.us/404');
const responseJson = await response.json();
return responseJson;
}
features/recipeList/recipeListSlice.js
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import recipeListApi from "./recipeListApi";
const sliceName = "recipeList";
const initialState = {
loading: false,
error: null,
data: null
};
export const fetchRecipeList = createAsyncThunk("recipeList/fetchRecipeList", async (thunkAPI) => {
const response = await recipeListApi();
return JSON.stringify(response);
});
const recipeListSlice = createSlice({
name: sliceName,
initialState: initialState,
extraReducers: {
[fetchRecipeList.pending]: state => {
if (!state.loading) {
state.loading = true;
}
},
[fetchRecipeList.fulfilled]: (state, action) => {
if (state.loading) {
state.loading = false;
state.data = action.payload;
}
},
[fetchRecipeList.rejected]: (state, action) => {
if (state.loading) {
state.loading = false;
state.error = action.payload;
}
}
}
});
export const recipeListReducer = recipeListSlice.reducer;
components/RecipeList.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchRecipeList } from '../features/recipeList/recipeListSlice';
export const RecipeList = () => {
const recipeList = useSelector(state => state.recipeList);
const dispatch = useDispatch();
/* Equivalent to componentDidMount() */
useEffect(() => {
dispatch(fetchRecipeList());
}, []);
return <>
{recipeList.loading && <h1>Loading</h1>}
{!recipeList.loading && recipeList.error !== null && <h1>Error</h1>}
{!recipeList.loading && recipeList.error === null && <h1>Loaded successfully</h1>}
</>;
}
Check if the response had a state of ok - or whatever condition you want to check your response for - and return a rejected promise like so:
export default async function recipeListApi(localApiKey) {
const response = await fetch('https://httpstat.us/404');
if(!response.ok) {
return Promise.reject();
}
return await response.json();
}
if your slice [rejected], the state.error should be receive from action.error
// features/recipeList/recipeListSlice.js
[fetchRecipeList.rejected]: (state, action) => {
if (state.loading) {
state.loading = false;
state.error = action.error;
}
}

Categories

Resources