Dispatch an action from ApolloClient onError - redux

I am using Redux and Apollo Client in my project. I would like to dispatch an action to trigger a message bar on top, stating the session is expired, when GraphQL endpoint returns 401 (Session expired).
But the client object is not an component, I cannot use useDispatch here, is there any way I can dispatch an action in onError?
import { InMemoryCache } from "apollo-cache-inmemory";
import ApolloClient from "apollo-client";
import { onError } from "apollo-link-error";
import { HttpLink } from "apollo-link-http";
const customFetch = (uri, options) => {
return fetch(uri, options)
.then(response => {
console.log("response fetch", response)
switch (response.status) {
case 400:
case 401:
case 500:
case 504:
return Promise.reject(response);
default:
return response;
}
});
};
const requestLink = new HttpLink({
uri: "/graphql",
fetch: customFetch,
});
const errorLink = onError(({ graphQLErrors, networkError, response }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, extensions }) => {
console.log(`[GraphQL error]: Message: ${message}, Location: ${extensions.code}`);
});
if (networkError) {
// TODO: Dispatch error to trigger a message bar component appear
console.log("[GraphQL networkError], ", networkError);
}
});
const link = errorLink.concat(requestLink);
const client = new ApolloClient({
link: errorLink.concat(requestLink),
cache: new InMemoryCache({
addTypename: false,
}),
});
export default client;

Related

Infinite loop on Redux saga yield call using Axios with JWT tokens

I have been trying to obtain data using Axios through Redux-saga using Redux-toolkit & react. It appears that intercepting a saga call with a token gets redux-saga in an infinite loop? Or is it because of my watchers?
I have recently been learning how to program so my skills in all areas are not yet great, hope you dont mind the way the code is written as I have been following tutorials mostly.
On handleSubmit from a Header.tsx to dispatch
const handleSubmit = (e) => {
e.preventDefault();
dispatch(getCurrentUser());
};
my rootSaga.tsx includes all watcherSagas notices the dispatch for getCurrentUser()
import { takeLatest } from "redux-saga/effects";
import {
handleLogInUser,
handleGetCurrentUser,
handleSetCurrentUser,
} from "./handlers/user";
import {
logInUser,
getCurrentUser,
setCurrentUser,
} from "../slices/user/userSlice";
export function* watcherSaga() {
yield takeLatest(logInUser.type, handleLogInUser);
yield takeLatest(getCurrentUser.type, handleGetCurrentUser);
yield takeLatest(setCurrentUser.type, handleSetCurrentUser);
}
the watcher calls handleGetCurrentUser for the saga located in user.tsx file in handler folder:
import { call, put } from "redux-saga/effects";
import { setCurrentUser } from "../../slices/user/userSlice";
import { requestLogInUser, requestGetCurrentUser } from "../requests/user";
export function* handleLogInUser(action) {
try {
console.log(action + "in handleLogInUser");
yield call(requestLogInUser(action));
} catch (error) {
console.log(error);
}
}
export function* handleGetCurrentUser(action) {
try {
const response = yield call(requestGetCurrentUser);
const userData = response;
yield put(setCurrentUser({ ...userData }));
} catch (error) {
console.log(error);
}
}
Which then uses yield call to requestGetCurrentUser which fires off the request to the following user.tsx in requests folder
import axiosInstance from "../../../axios/Axios";
export function requestGetCurrentUser() {
return axiosInstance.request({ method: "get", url: "/user/currentUser/" });
}
The response is given back and put in const userData, I consoleLog()'d the handler and discovered the following:
it will reach the handler successfully
go to the yield call
obtain the data successfully
return the data back to the handler
then it restarts the entire yield call again?
It also never makes it back to the userSlice in order to put the data.
axiosInstance in my axios.tsx file which includes the interceptor and gets the access_token and adds it to the header.
import axios from "axios";
const baseURL = "http://127.0.0.1:8000/api/";
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"),
"Content-Type": "application/json",
accept: "application/json",
},
});
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error.config;
if (typeof error.response === "undefined") {
alert(
"A server/network error occurred. " +
"Looks like CORS might be the problem. " +
"Sorry about this - we will get it fixed shortly."
);
return Promise.reject(error);
}
if (
error.response.status === 401 &&
originalRequest.url === baseURL + "token/refresh/"
) {
window.location.href = "/login/";
return Promise.reject(error);
}
if (
error.response.data.code === "token_not_valid" &&
error.response.status === 401 &&
error.response.statusText === "Unauthorized"
) {
const refreshToken = localStorage.getItem("refresh_token");
if (refreshToken) {
const tokenParts = JSON.parse(atob(refreshToken.split(".")[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
console.log(tokenParts.exp);
if (tokenParts.exp > now) {
return axiosInstance
.post("/token/refresh/", {
refresh: refreshToken,
})
.then((response) => {
localStorage.setItem("access_token", response.data.access);
localStorage.setItem("refresh_token", response.data.refresh);
axiosInstance.defaults.headers["Authorization"] =
"JWT " + response.data.access;
originalRequest.headers["Authorization"] =
"JWT " + response.data.access;
return axiosInstance(originalRequest);
})
.catch((err) => {
console.log(err);
});
} else {
console.log("Refresh token is expired", tokenParts.exp, now);
window.location.href = "/login/";
}
} else {
console.log("Refresh token not available.");
window.location.href = "/login/";
}
}
// specific error handling done elsewhere
return Promise.reject(error);
}
);
export default axiosInstance;
The userSlice.tsx
import { createSlice } from "#reduxjs/toolkit";
const userSlice = createSlice({
name: "user",
initialState: {},
reducers: {
logInUser(state, action) {},
getCurrentUser() {},
setCurrentUser(state, action) {
const userData = action.payload;
console.log(userData + "we are now back in slice");
return { ...state, ...userData };
},
},
});
export const { logInUser, getCurrentUser, setCurrentUser } = userSlice.actions;
export default userSlice.reducer;
I discovered that if I were to remove the authorization token it only fires off once and gets out of the infinite loop since it throws the unauthorised error.
Any suggestions would be greatly appreciated, thanks!
Apologies for getting back so late, I managed to fix it a while ago by pure chance and I dont exactly understand why.
But I believe what fixed it were the following two things:
Changing the useEffect that dispatched the action and ensuring that the handler returned data that the useEffect was expecting to be updated.
In the handler I deconstructed the userData to { userData } which I believe means that the data returned from the axios request is not the entire request but the actual returned data.
my handler
export function* handleGetCurrentUser() {
try {
console.log("in request get user");
const response = yield call(requestGetCurrentUser);
const { data } = response;
yield put(setCurrentUser({ ...data }));
} catch (error) {
console.log(error);
}
}
I forgot to add my useEffect to the post, which created the action.
my useEffect in the App.tsx would dispatch the call when the App was rendered for the first time. However because the returned data did not update what was expected it kept rerendering.
I cant exactly remember what my useEffect was but currently it is the following:
my useEffect in App.tsx
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentUser());
}, [dispatch]);
const user = useSelector((state) => state.user);

Redux saga is is going to catch() instead of putting the action

I have this action in userActions.js:
const receiveFilialData = (data) => {
return {
type: "RECEIVE_FILIAL_DATA",
payload: data
}
}
export default {
receiveFilialData
}
In my login component, I'm making a dispatch to get some data from a saga:
const Login = props => {
const dispatch = useDispatch()
const filiais = useSelector(state => state.filiais)
useEffect(() => {
dispatch({type: 'GET_FILIAIS_REQUEST'})
}, [dispatch, filiais])
}
In LoginService.js I'm making the HTTP request:
const fetchFiliais = async () => {
const response = await fetch(`${baseUrlApi.baseUrlApi}/filiais`);
const responseJson = await response.json()
return responseJson
}
export default {
fetchFiliais
}
And I create a saga to make this request when there's a GET_FILIAIS_REQUEST dispatch:
// worker Saga: will be fired on GET_FILIAIS_REQUEST actions
function* fetchFiliais(action) {
try {
const data = yield call(LoginService.fetchFiliais())
console.log(data)
yield put({type: "RECEIVE_FILIAL_DATA", data});
} catch (e) {
yield put({type: 'FETCH_FILIAIS_FAILED', message: e.message})
}
}
/*
Starts fetchFiliais on each dispatched `GET_FILIAIS_REQUEST` action.
*/
function* mySaga() {
yield takeEvery("GET_FILIAIS_REQUEST", fetchFiliais);
}
If I put a console.log(response.json) in the LoginService file, fetchFiliais is printing my response, but my console.log(data) after yield call() is not running; the control flow is going to catch(e) and throwing:
type: "FETCH_FILIAIS_FAILED" message: "call: argument of type
{context, fn} has undefined or null fn"
Why?
Fixed, putting LoginService.fetchFiliais instead LoginService.fetchFiliais())
This is not a function but a const

Cannot proper handle error in ajax from rxjs

I write application in React an Redux and I have a case that I have to send request to some resource when specific action was called and response of type 'text/html' write to state. This resource can return status 200 or 404, and I cannot write correct test for case with response 404.
To run tests I use jest library.
Action.js:
export const actions = {
GET_RESOURCE_SUCCESS: 'GET_RESOURCE_SUCCESS',
GET_RESOURCE_FAILURE: 'GET_RESOURCE_FAILURE'
};
export const getResourceSuccess = (response) => ({
type: actions.GET_RESOURCE_SUCCESS,
payload: response
});
export const getResourceFailure = () => ({
type: actions.GET_RESOURCE_FAILURE
});
Reducer.js:
import { handleActions } from 'redux-actions';
import { actions } from './Action';
const initialState = {
content: ''
};
export const getResourceReducer = handleActions(
{
[actions.GET_RESOURCE_SUCCESS]: (state, action) => ({ ...state, content: action.payload })
},
{
[actions.GET_RESOURCE_FAILURE]: () => ({ initialState })
},
initialState
);
In brief: when resource returns status 200 and content exists I want to overwrite content from initialState and call action GET_RESOURCE_SUCCESS, when resource returns status 404 and content doesn't exist I want not to overwrite the content and call action GET_RESOURCE_FAILURE.
GetResourceEpic.js:
import { ajax } from 'rxjs/observable/dom/ajax';
import { combineEpics } from 'redux-observable';
import { Observable } from 'rxjs';
import { getResourceSuccess, getResourceFailure } from '../Action';
const specificActionTypes = [
'SPECIFIC_ACTION_ONE',
'SPECIFIC_ACTION_TWO'
];
const getResource = () => ajax({
method: 'GET',
url: 'http://example.com',
headers: {
Accept: 'text/html'
},
crossDomain: true,
responseType: 'text/html'
});
const getResourceEpic = (action$, store) => action$
.filter(action => specificActionTypes.includes(action.type))
.flatMap(() => getResource()
// when response has status 200 and field response call getResourceSuccess
.map(({ response }) => getResourceSuccess(response))
// when response has status 404 and doesn't have field response call getResourceFailure
.catch(() => {
// helper statement to show in browser that the .catch() was called
console.log('Error');
return getResourceFailure();
})
);
export default combineEpics(
getResourceEpic
);
And it works in generally, but I get two errors:
first:
Uncaught TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
at Object.subscribeToResult (subscribeToResult.js:73)
at CatchSubscriber../node_modules/rxjs/operator/catch.js.CatchSubscriber.error (catch.js:111)
at MapSubscriber../node_modules/rxjs/Subscriber.js.Subscriber._error (Subscriber.js:128)
at MapSubscriber../node_modules/rxjs/Subscriber.js.Subscriber.error (Subscriber.js:102)
at AjaxSubscriber../node_modules/rxjs/Subscriber.js.Subscriber._error (Subscriber.js:128)
at AjaxSubscriber../node_modules/rxjs/Subscriber.js.Subscriber.error (Subscriber.js:102)
at XMLHttpRequest.xhrReadyStateChange (AjaxObservable.js:327)
at XMLHttpRequest.d (raven.js:363)
and second:
Could not consume error: TypeError: Cannot read property 'length' of null
at getLinesAround (http://localhost:3000/static/js/bundle.js:47197:74)
at http://localhost:3000/static/js/bundle.js:47537:402
at Array.map (native)
at _callee2$ (http://localhost:3000/static/js/bundle.js:47517:54)
at tryCatch (http://localhost:3000/static/js/bundle.js:58220:40)
at Generator.invoke [as _invoke] (http://localhost:3000/static/js/bundle.js:58458:22)
at Generator.prototype.(anonymous function) [as next] (http://localhost:3000/static/js/bundle.js:58272:21)
at step (http://localhost:3000/static/js/bundle.js:47553:191)
at http://localhost:3000/static/js/bundle.js:47553:361
raven.js:51
And mentioned above problems don't allow me to write a tests, because only responses with status 200 pass the tests, other throws errors.
it('should dispatch GET_RESOURCE_SUCCESS when SPECIFIC_ACTION_ONE was dispatched', async () => {
store = mockStore();
const response = 'some content';
nock('http://example.com')
.get('/')
.reply(200, response);
const payload = { type: 'SPECIFIC_ACTION_ONE' };
// specificActionOne() produces and action of type 'SPECFIC_ACTION_ONE'
const action$ = ActionsObservable.of(specificActionOne(payload));
const resultAction = await getResourceEpic(action$, store).toPromise();
expect(resultAction)
.toEqual(getResourceSuccess(response));
});
And test above pass, but case with status equal to 404 doesn't pass:
it('should dispatch GET_RESOURCE_FAILURE when SPECIFIC_ACTION_ONE was dispatched', async () => {
store = mockStore();
nock('http://example.com')
.get('/')
.reply(404);
const payload = { type: 'SPECIFIC_ACTION_ONE' };
const action$ = ActionsObservable.of(specificActionOne(payload));
const resultAction = await getResourceEpic(action$, store).toPromise();
expect(resultAction)
.toEqual(getResourceFailure());
});
And test above doesn't pass and I get the result:
TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
at Object.subscribeToResult (node_modules/rxjs/util/subscribeToResult.js:73:27)
at CatchSubscriber.Object.<anonymous>.CatchSubscriber.error (node_modules/rxjs/operator/catch.js:111:42)
at MapSubscriber.Object.<anonymous>.Subscriber._error (node_modules/rxjs/Subscriber.js:128:26)
at MapSubscriber.Object.<anonymous>.Subscriber.error (node_modules/rxjs/Subscriber.js:102:18)
at AjaxSubscriber.Object.<anonymous>.Subscriber._error (node_modules/rxjs/Subscriber.js:128:26)
at AjaxSubscriber.Object.<anonymous>.Subscriber.error (node_modules/rxjs/Subscriber.js:102:18)
at XMLHttpRequest.xhrReadyStateChange [as onreadystatechange] (node_modules/rxjs/observable/dom/AjaxObservable.js:327:32)
at XMLHttpRequest.callback.(anonymous function) (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:289:32)
at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:219:27)
at invokeInlineListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:166:7)
at EventTargetImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:122:7)
at EventTargetImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:87:17)
at XMLHttpRequest.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:61:35)
at readyStateChange (node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:829:9)
at Request.properties.client.on (node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:947:7)
at Request.emit (events.js:132:15)
at IncomingMessage.<anonymous> (node_modules/request/request.js:1085:12)
at Object.onceWrapper (events.js:219:13)
at IncomingMessage.emit (events.js:132:15)
at endReadableNT (_stream_readable.js:1101:12)
at process._tickCallback (internal/process/next_tick.js:114:19)
The function passed to catch has to return an observable. You are returning an action.
Instead, you should do this:
import { of } from 'rxjs/observable/of';
...
.catch(() => {
console.log('Error');
return of(getResourceFailure());
})

redux refresh token middleware

I have a middleware that can go to the refresh token before the next action runs and then run the other action when the access token expires.
But if I make more than one request at a time and the access token is over, I am trying to get as much refresh token as I am requesting. I am checking the isLoading property in state to prevent this. But after the request, isLoading value is true in the reducer, it seems to be false in the middleware, so it requests again and again.
I am sending refreshTokenPromise in fetching_refresh_token action, but I never get state.refreshTokenPromise, it is always undefined.
I definitely have a problem with the state.
So here is my question, how can I access the changing state value in middleware?
Refresh token middleware: (this version hits the endpoint multiple times)
import { AsyncStorage } from 'react-native';
import { MIN_TOKEN_LIFESPAN } from 'react-native-dotenv';
import moment from 'moment';
import Api from '../lib/api';
import {
FETCHING_REFRESH_TOKEN,
FETCHING_REFRESH_TOKEN_SUCCESS,
FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';
export default function tokenMiddleware({ dispatch, getState }) {
return next => async (action) => {
if (typeof action === 'function') {
const state = getState();
if (state) {
const expiresIn = await AsyncStorage.getItem('EXPIRES_IN');
if (expiresIn && isExpired(JSON.parse(expiresIn))) {
if (!state.refreshToken.isLoading) {
return refreshToken(dispatch).then(() => next(action));
}
return state.refreshTokenPromise.then(() => next(action));
}
}
}
return next(action);
};
}
async function refreshToken(dispatch) {
const clientId = await AsyncStorage.getItem('CLIENT_ID');
const clientSecret = await AsyncStorage.getItem('CLIENT_SECRET');
const refreshToken1 = await AsyncStorage.getItem('REFRESH_TOKEN');
const userObject = {
grant_type: 'refresh_token',
client_id: JSON.parse(clientId),
client_secret: JSON.parse(clientSecret),
refresh_token: refreshToken1,
};
const userParams = Object.keys(userObject).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(userObject[key])).join('&');
const refreshTokenPromise = Api.post('/token', userParams).then(async (res) => {
await AsyncStorage.setItem('ACCESS_TOKEN', res.access_token);
await AsyncStorage.setItem('REFRESH_TOKEN', res.refresh_token);
await AsyncStorage.setItem('EXPIRES_IN', JSON.stringify(res['.expires']));
dispatch({
type: FETCHING_REFRESH_TOKEN_SUCCESS,
data: res,
});
return res ? Promise.resolve(res) : Promise.reject({
message: 'could not refresh token',
});
}).catch((err) => {
dispatch({
type: FETCHING_REFRESH_TOKEN_FAILURE,
});
throw err;
});
dispatch({
type: FETCHING_REFRESH_TOKEN,
refreshTokenPromise,
});
return refreshTokenPromise;
}
function isExpired(expiresIn) {
return moment(expiresIn).diff(moment(), 'seconds') < MIN_TOKEN_LIFESPAN;
}
Refresh token reducer:
import {
FETCHING_REFRESH_TOKEN,
FETCHING_REFRESH_TOKEN_SUCCESS,
FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';
const initialState = {
token: [],
isLoading: false,
error: false,
};
export default function refreshTokenReducer(state = initialState, action) {
switch (action.type) {
case FETCHING_REFRESH_TOKEN:
return {
...state,
token: [],
isLoading: true,
};
case FETCHING_REFRESH_TOKEN_SUCCESS:
return {
...state,
isLoading: false,
token: action.data,
};
case FETCHING_REFRESH_TOKEN_FAILURE:
return {
...state,
isLoading: false,
error: true,
};
default:
return state;
}
}
In the meantime, when I send it to the getState to refreshToken function, I get to the changing state value in the refreshToken. But in this version, the refresh token goes to other actions without being refreshed.
Monkey Patched version: (this version only makes 1 request)
import { AsyncStorage } from 'react-native';
import { MIN_TOKEN_LIFESPAN } from 'react-native-dotenv';
import moment from 'moment';
import Api from '../lib/api';
import {
FETCHING_REFRESH_TOKEN,
FETCHING_REFRESH_TOKEN_SUCCESS,
FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';
export default function tokenMiddleware({ dispatch, getState }) {
return next => async (action) => {
if (typeof action === 'function') {
const state = getState();
if (state) {
const expiresIn = await AsyncStorage.getItem('EXPIRES_IN');
if (expiresIn && isExpired(JSON.parse(expiresIn))) {
if (!state.refreshTokenPromise) {
return refreshToken(dispatch, getState).then(() => next(action));
}
return state.refreshTokenPromise.then(() => next(action));
}
}
}
return next(action);
};
}
async function refreshToken(dispatch, getState) {
const clientId = await AsyncStorage.getItem('CLIENT_ID');
const clientSecret = await AsyncStorage.getItem('CLIENT_SECRET');
const refreshToken1 = await AsyncStorage.getItem('REFRESH_TOKEN');
const userObject = {
grant_type: 'refresh_token',
client_id: JSON.parse(clientId),
client_secret: JSON.parse(clientSecret),
refresh_token: refreshToken1,
};
if (!getState().refreshToken.isLoading) {
const userParams = Object.keys(userObject).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(userObject[key])).join('&');
const refreshTokenPromise = Api.post('/token', userParams).then(async (res) => {
await AsyncStorage.setItem('ACCESS_TOKEN', res.access_token);
await AsyncStorage.setItem('REFRESH_TOKEN', res.refresh_token);
await AsyncStorage.setItem('EXPIRES_IN', JSON.stringify(res['.expires']));
dispatch({
type: FETCHING_REFRESH_TOKEN_SUCCESS,
data: res,
});
return res ? Promise.resolve(res) : Promise.reject({
message: 'could not refresh token',
});
}).catch((err) => {
dispatch({
type: FETCHING_REFRESH_TOKEN_FAILURE,
});
throw err;
});
dispatch({
type: FETCHING_REFRESH_TOKEN,
refreshTokenPromise,
});
return refreshTokenPromise;
}
}
function isExpired(expiresIn) {
return moment(expiresIn).diff(moment(), 'seconds') < MIN_TOKEN_LIFESPAN;
}
Thank you.
I solved this problem using axios middlewares. I think is pretty nice.
import { AsyncStorage } from 'react-native';
import Config from 'react-native-config';
import axios from 'axios';
import { store } from '../store';
import { refreshToken } from '../actions/refreshToken'; // eslint-disable-line
const instance = axios.create({
baseURL: Config.API_URL,
});
let authTokenRequest;
function resetAuthTokenRequest() {
authTokenRequest = null;
}
async function getAuthToken() {
const clientRefreshToken = await AsyncStorage.getItem('clientRefreshToken');
if (!authTokenRequest) {
authTokenRequest = store.dispatch(refreshToken(clientRefreshToken));
authTokenRequest.then(
() => {
const {
token: { payload },
} = store.getState();
// save payload to async storage
},
() => {
resetAuthTokenRequest();
},
);
}
return authTokenRequest;
}
instance.interceptors.response.use(
response => response,
async (error) => {
const originalRequest = error.config;
if (
error.response.status === 401
&& !originalRequest._retry // eslint-disable-line no-underscore-dangle
) {
return getAuthToken()
.then(() => {
const {
token: {
payload: { 'access-token': accessToken, client, uid },
},
} = store.getState();
originalRequest.headers['access-token'] = accessToken;
originalRequest.headers.client = client;
originalRequest.headers.uid = uid;
originalRequest._retry = true; // eslint-disable-line no-underscore-dangle
return axios(originalRequest);
})
.catch(err => Promise.reject(err));
}
return Promise.reject(error);
},
);
export default instance;
If you have a problem, do not hesitate to ask.
you could benefit from redux-sagas
https://github.com/redux-saga/redux-saga
redux-sagas is just background runner which monitors your actions and can react when some specific action is met. You can listen for all actions and react to all or you can react to only latest as mentioned in comments
https://redux-saga.js.org/docs/api/#takelatestpattern-saga-args
while redux-thunk is just another way to create actions on the go and wait for some I/O to happen and then create some more actions when I/O is done. It's more like synced code pattern and redux-sagas is more like multi-threaded. On main thread you have your app running and on background thread you have sagas monitors and reactions

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