I am checking user is valid or not after displaying splash screen. So In my splash screen I checked the data from redux saga like below
Hide_Splash_Screen=()=>{
this.setState({
isVisible:false
})
if(this.props.phoneNumberData !== null )
{
this.props.navigation.navigate('HomeScreen')
}
else
{
this.props.navigation.navigate('PhoneVerification')
}
}
componentDidMount(){
setTimeout(()=>{
this.Hide_Splash_Screen();
}, 2000);
this.props.fetchSavePhoneNumber()
}
So if the user has install the app for firstime then PhoneVerification Screen is prompted and not it will directly go into HomeScreen
So here is my logic to check phone verification with otp and then store as a new user in saga
handleSendCode=()=>{
var phoneno = '+91'+this.state.phone
firebase
.auth()
.signInWithPhoneNumber(phoneno)
.then(confirmResult => {
this.setState({ confirmResult })
})
.catch(error => {
alert(error.message)
console.log(error)
})
this.setState({showotpScreen:true})
}
checkOtp=()=>{
this.state.confirmResult
.confirm(this.state.otp)
.then(user => {
this.props.NewPhoneNumber(user)
this.props.navigation.navigate('HomeScreen')
})
.catch(error => {
alert(error.message)
console.log(error)
})
}
For ReduxSaga implementation I take two different action and one reducer like this
phoneaction
export const fetchSavePhoneNumber = () => ({
type: 'FETCH_SAVE_PHONENUMBER',
});
export const NewPhoneNumber = (data) => ({
type:'SAVE_NEW_PHONENUMBER',
payload: data
});
phoneReducer
const initialState = {
phoneNumberData: null,
};
export default (state = initialState, { type, payload }) => {
switch(type){
case 'SAVE_NEW_PHONENUMBER':
return{
...state,
phoneNumberData: payload,
};
case 'IS_VALIDATING':
return{
...state,
};
default:
return state;
};
}
And my Two Saga will look like this
NewPhoneNumber
import { call, put, select, takeLatest } from 'redux-saga/effects';
import AsyncStorage from '#react-native-community/async-storage';
const phoneno = state => state.phone.phoneNumberData ;
function* PhoneVerifyTask(action){
const phoneNumberData = yield select(phoneno);
try{
yield call(AsyncStorage.setItem,'phoneVerify',JSON.stringify(phoneNumberData));
yield put({ type: 'SAVE_NEW_PHONENUMBER', payload: phoneNumberData });
}
catch(error){
console.log(error)
}
}
function* NewPhoneNumber(){
yield takeLatest('SAVE_NEW_PHONENUMBER',PhoneVerifyTask)
}
export default NewPhoneNumber;
FetchSavePhoneNumber
import { call, put, takeLatest } from 'redux-saga/effects';
import AsyncStorage from '#react-native-community/async-storage';
function* fetchVerifyPhoneNumber(action){
yield put({
type: 'IS_VALIDATING',
});
try
{
const response = yield call(AsyncStorage.getItem,'phoneVerify')
yield put({
type: 'SAVE_NEW_PHONENUMBER',
payload: JSON.parse(response) || null
});
}
catch(error){
console.log(e);
yield put({
type: 'SAVE_NEW_PHONENUMBER',
payload: {
phoneNumberData: null
},
});
}
}
function* FetchSavePhoneNumber(){
yield takeLatest('FETCH_SAVE_PHONENUMBER',fetchVerifyPhoneNumber)
}
export default FetchSavePhoneNumber;
But after successfully storing newPhoneNumberdata and fetchingExisting PhoneNumberdata my whole redux act as a infinite loop
This is probably because your component that calls FETCH_... is re-rendered and calls fetch another time.
I'd suggest either calling the action higher up in your app (in your App.js constructor maybe). You can also fire sagas once on app load in your generator directly.
I have an application with like button. User can like multiple posts in quick succession. I send the action to update likecount and add like/user record through a redux action.
export const likePost = (payload) => (dispatch) => {
dispatch({
type: "LIKE_POST",
payload,
});
};
In the saga on successful update, the control of action comes in both cases but LIKE_POST_SUCCESSFUL is triggered only for the last.
function* requestLikePost(action) {
const { postId } = action.payload;
try {
const response = yield call(callLikePostsApi, postId);
yield put({
type: "LIKE_POST_SUCCESSFUL",
payload: response.data,
});
} catch (error) {
yield put({
type: "LIKE_POST_FAILED",
payload: error.response.data,
});
}
}
These are recieving action in reducer. The LIKE_POST is triggered two times as expected but not the LIKE_POST_SUCCESSFUL, its triggered only for the last though both reached .
case "LIKE_POST":
return {
...state,
errors: {},
};
case "LIKE_POST_SUCCESSFUL":
updatedPosts = state.posts.map((post) => {
if (post.postId === action.payload.postId) {
return action.payload;
}
return post;
});
updatedLikes = [
...state.likes,
{ userName: state.profile.userName, postId: action.payload.postId },
];
console.log("updatedLikes", updatedLikes, action.payload);
return {
...state,
posts: updatedPosts,
likes: updatedLikes,
loading: false,
};
API call
const callLikePostsApi = (postId) => axios.get(`/post/${postId}/like`);
Are you using takeLatest() effect for your saga function requestLikePost? It will take only latest action call into consideration and aborts all the previous calls if it happens in quick succession.
Use takeEvery() saga effect instead.
I currently am fetching some balances via a redux getBalances method. When the app initializes it sets 'balances' to the JSON of information, however when I call getBalances again it doesn't re-set the balances (no idea why).
So right now, I'm manually trying to update the balances by calling the getBalances method then setting the result of that to balances, however I'm running into walls.
All I'd like to do is getBalances again and merely set this to balances, however I'm not sure how I'd do this in redux.
// Sequence of events (all of these are in different files of course)
// Action Call
export const getBalances = exchange =>
action(actionTypes.GET_BALANCES.REQUEST, { exchange })
// API Call
export const getBalances = ({ userId, exchange }) =>
API.request(`/wallets/${userId}/${exchange}`, 'GET')
Full saga
import { fork, takeEvery, takeLatest, select, put, call, throttle } from 'redux-saga/effects'
import { NavigationActions } from 'react-navigation'
import * as actionTypes from '../action-types/exchanges.action-types'
import * as API from '../api'
import { storeType } from '../reducers'
import { async, delay } from './asyncSaga'
import { asyncAction } from './asyncAction'
let getBalanceCount = 0
export function* getBalances(action) {
getBalanceCount++
const state: storeType = yield select()
yield fork(async, action, API.getBalances, {
exchange: state.exchanges.selectedExchange._id,
userId: state.auth.userId,
})
if (getBalanceCount > 1) {
getBalanceCount--
return
}
yield delay(10000)
if (state.auth.token && state.auth.status === 'success')
yield put({ type: action.type, payload: {} })
/*
if (state.auth.token && state.auth.status === 'success' && state.auth.phoneVerified)
yield put({ type: action.type, payload: {} }) */
}
export function* getExchanges(action) {
const state: storeType = yield select()
yield fork(async, action, API.getExchanges, { userId: state.auth.userId })
}
export function* getExchangesSuccess(action) {
const state: storeType = yield select()
if (state.exchanges.exchanges.length > 0) {
yield put({ type: actionTypes.GET_BALANCES.REQUEST, payload: {} })
}
}
export function* addExchange(action) {
const state: storeType = yield select()
yield fork(async, action, API.addExchange, { ...action.payload, userId: state.auth.userId })
}
export function* addExchangeSuccess(action) {
yield put(
NavigationActions.navigate({
routeName: 'wallets',
params: { transition: 'slideToTop' },
}),
)
}
export function* updatePrices(action) {
const async = asyncAction(action.type)
const state = yield select()
try {
const res = yield call(API.getSymbolPriceTicker)
yield put(async.success(res))
} catch (error) {
yield put(async.failure(error))
}
yield delay(10000)
if (state.auth.token && state.auth.status === 'success' && state.auth.phoneVerified)
yield put({ type: action.type, payload: {} })
}
export function* updateDaily(action) {
const async = asyncAction(action.type)
try {
const res = yield call(API.getdayChangeTicker)
yield put(async.success(res))
} catch (error) {
yield put(async.failure(error))
}
}
export function* getFriendExchange(action) {
yield fork(async, action, API.getExchanges, { userId: action.payload.userId })
}
export function* selectExchange(action) {
yield put({ type: actionTypes.GET_BALANCES.REQUEST, payload: {} })
}
export function* exchangesSaga() {
yield takeEvery(actionTypes.GET_SYMBOL_PRICE_TICKER.REQUEST, updatePrices)
yield takeEvery(actionTypes.GET_DAY_CHANGE_TICKER.REQUEST, updateDaily)
yield takeLatest(actionTypes.GET_FRIEND_EXCHANGES.REQUEST, getFriendExchange)
yield takeLatest(actionTypes.GET_BALANCES.REQUEST, getBalances)
yield takeLatest(actionTypes.GET_EXCHANGES.REQUEST, getExchanges)
yield takeLatest(actionTypes.GET_EXCHANGES.SUCCESS, getExchangesSuccess)
yield takeLatest(actionTypes.ADD_EXCHANGE.REQUEST, addExchange)
yield takeLatest(actionTypes.ADD_EXCHANGE.SUCCESS, addExchangeSuccess)
yield takeLatest(actionTypes.SELECT_EXCHANGE, selectExchange)
}
Full Exchange Reducer
import { mergeDeepRight } from 'ramda'
import {
GET_BALANCES,
GET_EXCHANGES,
SELECT_EXCHANGE,
GET_SYMBOL_PRICE_TICKER,
GET_DAY_CHANGE_TICKER,
GET_FRIEND_EXCHANGES,
ADD_EXCHANGE,
} from '../action-types/exchanges.action-types'
import { LOG_OUT, VALIDATE_TOKEN } from '../action-types/login.action-types'
import { ExchangeService } from '../constants/types'
// Exchanges Reducer
export type exchangeState = {
status: string
_id: string
label: string
displayName: string
dayChangeTicker: any
symbolPriceTicker: any
balances: any,
}
export type exchangesState = {
status: string
selectedExchange: exchangeState
addExchange: {
status: string,
}
exchanges: Array<ExchangeService>
friendExchanges: Array<ExchangeService>,
}
const initialExchangeState: exchangeState = {
status: 'pending',
_id: '',
label: '',
displayName: null,
dayChangeTicker: {},
symbolPriceTicker: {},
balances: {},
}
const initialState: exchangesState = {
status: 'pending',
selectedExchange: {
status: 'pending',
_id: '',
label: '',
displayName: null,
dayChangeTicker: {},
symbolPriceTicker: {},
balances: {},
},
addExchange: {
status: 'pending',
},
exchanges: [],
friendExchanges: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case SELECT_EXCHANGE:
case GET_SYMBOL_PRICE_TICKER.SUCCESS:
case GET_DAY_CHANGE_TICKER.SUCCESS:
case GET_BALANCES.REQUEST:
case GET_BALANCES.SUCCESS:
case GET_BALANCES.FAILURE:
return { ...state, selectedExchange: selectedExchangeReducer(state.selectedExchange, action) }
case GET_EXCHANGES.REQUEST:
case GET_FRIEND_EXCHANGES.REQUEST:
return { ...state, status: 'loading' }
case GET_EXCHANGES.SUCCESS:
if (action.payload.exchanges.length > 0) {
return mergeDeepRight(state, {
exchanges: action.payload.exchanges,
selectedExchange: { ...action.payload.exchanges[0] },
status: 'success',
})
}
return { ...state, status: 'success' }
case GET_FRIEND_EXCHANGES.SUCCESS:
return { ...state, friendExchanges: action.payload.exchanges, status: 'success' }
case GET_EXCHANGES.FAILURE:
case GET_FRIEND_EXCHANGES.FAILURE:
return { ...state, message: action.payload.message, status: 'failure' }
case LOG_OUT.SUCCESS:
case VALIDATE_TOKEN.FAILURE:
return initialState
case ADD_EXCHANGE.REQUEST:
return { ...state, addExchange: { status: 'loading' } }
case ADD_EXCHANGE.SUCCESS:
return { ...state, addExchange: { status: 'success' } }
case ADD_EXCHANGE.FAILURE:
return { ...state, addExchange: { status: 'failure' } }
default:
return state
}
}
const selectedExchangeReducer = (state = initialExchangeState, action) => {
switch (action.type) {
case SELECT_EXCHANGE:
if (action.payload.exchange) {
return { ...state, ...action.payload.exchange }
}
return initialExchangeState
case GET_SYMBOL_PRICE_TICKER.SUCCESS:
const symbolPriceTicker = action.payload.data.data.reduce((result, ticker) => {
result[ticker.symbol] = ticker.price
return result
}, {})
return { ...state, symbolPriceTicker }
case GET_DAY_CHANGE_TICKER.SUCCESS:
const dayChangeTicker = action.payload.data.data.reduce((result, ticker) => {
result[ticker.symbol] = ticker.priceChangePercent
return result
}, {})
return { ...state, dayChangeTicker }
// Get selected exchange's balances
case GET_BALANCES.REQUEST:
return { ...state, status: 'loading' }
case GET_BALANCES.SUCCESS:
return {
...state,
balances: action.payload.balances,
status: 'success',
}
case GET_BALANCES.FAILURE:
return { ...state, balances: [], message: action.payload.message, status: 'failure' }
default:
return state
}
}
Physical function call (fetchData is my attempt at reassigning exchange.balances...)
// this.props.selectExchange(exchange) just selects the exchange then calls a GET_BALANCES.REQUEST
fetchData = (exchange) => {
const { selectedExchange } = this.props.exchanges
// const { exchanges } = this.props
// //console.log('TesterTesterTester: ' + JSON.stringify(this.props.selectExchange(exchange)))
// console.log('Test:' + JSON.stringify(this.props.getBalances(exchange.balances)))
// let vari = JSON.stringify(this.props.getBalances(exchange.balances))
// let newVari = JSON.parse(vari.slice(45, vari.length-2))
// exchange.balances = newVari
// console.log('Old Values: ' + JSON.stringify(exchange.balances))
console.log('Testt: ' + JSON.stringify(this.props.selectExchange(exchange.balances1)))
this.props.selectExchange(exchange.balances1)
console.log('This exchange after: ' + selectedExchange)
console.log('This is the balances: '+ JSON.stringify(selectedExchange.balances1))
exchange.balances = selectedExchange.balances1
console.log('Another one: ' + JSON.stringify(exchange.balances))
selectedExchange.balances1 = []
this.setState({ refreshing: false })
}
renderExchange = (exchange, index) => {
const { refreshing } = this.state
const { selectedExchange } = this.props.exchanges
const { symbolPriceTicker, dayChangeTicker } = selectedExchange
// I'm trying to alter exchange.balances
if (refreshing) {
this.fetchData(exchange)
}
return (
<View style={screenStyles.container}>
<ExchangeBox
balances={exchange.balances}
displayName={exchange.label}
symbolPriceTicker={symbolPriceTicker}
exchangeIndex={index}
onSend={this.onSend}
/>
<View style={screenStyles.largerContainer}>
{symbolPriceTicker && dayChangeTicker && exchange.balances && (
<ScrollView
style={screenStyles.walletContainer}
horizontal={true}
showsHorizontalScrollIndicator={false}
decelerationRate={0}
snapToInterval={100} //your element width
snapToAlignment={'center'}
>
{Object.keys(exchange.balances).map(
symbol =>
COIN_INFO[symbol] &&
symbolPriceTicker[`${symbol}USDT`] && (
<CoinContainer
key={symbol}
symbol={symbol}
available={exchange.balances[symbol].free}
price={symbolPriceTicker[`${symbol}USDT`]}
dayChange={dayChangeTicker[`${symbol}USDT`]}
/>
),
)}
</ScrollView>
)}
</View>
</View>
)
}
After messing with this I found that exchange.balances wasn't grabbing values because the .balances was a JSON extension of the JSON of exchange. I tried making all of the instance of balances elsewhere (like in the reducer balances1) and that didn't help much when trying to update.
Here's another call of balances in types.ts
export type ExchangeService = {
_id: string
label: string
displayName: string
balances: any,
}
Thank you so much #Dylan for walking through this with me
As discussed in the comments:
You're slightly overthinking how you're managing your state via fetchData. It appears you're attempting to dispatch an action and use the results in the same render cycle, which will have inconsistent results at best using Redux.
Instead, when using Redux with React, you should almost completely rely on Redux to handle your state management. Your React components should only be used for dispatching Redux actions and displaying incoming data, as per the following data flow:
Component dispatches an action to the store.
The action is processed by your sagas and reducers to update the state in the store.
Redux updates the props being provided to the component via connect().
The updated props trigger a re-render of the component.
The updated state is now available to the component via this.props in your render() function on the triggered render cycle.
As this relates to your component, your fetchData function would probably be simplified to something like this:
fetchData = exchange => {
this.props.selectExchange(exchange);
// ...any additional action dispatches required to fetch data.
}
If your reducers and sagas are written correctly (which they appear to be), then your Redux state will be updated asynchronously. When the update is complete, your components props will be updated and a re-render triggered. Then, in your render() function, all data you display from the state should be derived from this.props. By doing this, you largely guarantee that the display is up-to-date:
render() {
const exchange = this.props.selectedExchange;
return (
<View style={screenStyles.container}>
<ExchangeBox
balances={exchange.balances}
displayName={exchange.label}
// ... more props
/>
//... more components
</View>
);
}
At this point, your component is setup with a simple and idiomatic Redux data flow. If you encounter any issues with state updates from this point, you can start looking at your sagas/reducers for issues.
Below is my original answer which talked about a potential issue with the posted sagas, which I'll keep for completeness.
Thanks for the clarifying edits. Took me awhile to understand, because this saga structure is really unusual, but I think I have an idea of what's going on here. Please correct me if I make any wrong assumptions.
yield delay(10000)
if (state.auth.token && state.auth.status === 'success')
yield put({ type: action.type, payload: {} })
I assume the purpose of this is to update the balances every 10 seconds once the saga has been initially kicked off. I'm also assuming you have getBalancesCount to limit the number of instances of getBalances looping at once. Lets walk through how this happens:
Initial dispatch -> yield takeLatest(actionTypes.GET_BALANCES.REQUEST, getBalances) kicks off getBalances.
getBalances hits getBalanceCount++, so getBalanceCount == 1
getBalances is repeated, due to put({ type: action.type, payload: {} })
getBalances hits getBalanceCount++, so getBalanceCount == 2
getBalances hits if (getBalanceCount > 1), satisfies the condition, decrements getBalanceCount to 1 and exits.
Now, I'm assuming yield fork(async, action, API.getBalances...) eventually dispatches GET_BALANCES.SUCCESS in asyncSaga, so it'll continue to work each time you dispatch GET_BALANCES.REQUEST from outside the saga.
You could fix up the logic for getBalancesCount. However, we don't need a counter at all to limit the number of concurrent getBalances running at once. This is already built into takeLatest:
Each time an action is dispatched to the store. And if this action matches pattern, takeLatest starts a new saga task in the background. If a saga task was started previously (on the last action dispatched before the actual action), and if this task is still running, the task will be cancelled.
(See: https://redux-saga.js.org/docs/api/)
So all you really have to do is remove your custom logic:
export function* getBalances(action) {
const state: storeType = yield select()
yield fork(async, action, API.getBalances, {
exchange: state.exchanges.selectedExchange._id,
userId: state.auth.userId,
})
yield delay(10000)
if (state.auth.token && state.auth.status === 'success')
yield put({ type: action.type, payload: {} })
}
}
In addition, repeating a saga by dispatching the same action from within the saga is kind of an anti-pattern. while(true) tends to be more idiomatic despite looking strange:
export function* getBalances(action) {
while(true) {
const state: storeType = yield select()
yield fork(async, action, API.getBalances, {
exchange: state.exchanges.selectedExchange._id,
userId: state.auth.userId,
})
yield delay(10000);
if (!state.auth.token || state.auth.status !== 'success')
return;
}
}
}
Though, if you have other things consuming GET_BALANCES.REQUEST for some reason, this might not work for you. In that case, I'd be using separate actions though. (EDIT: I re-read your reducer, and you're indeed using the action to set a loading state. In this case, your approach is probably fine.)
I have a "My Profile" form that displays the details of the user.
Api call to fetch user data is as follows.
componentDidMount() {
this.props.getUserDetails();
}
Saga file is as follows
function* fetchUserDetails() {
try {
const response = yield call(userDetailsApi);
const user = response.data.user;
// dispatch a success action to the store
yield put({ type: types.USER_DETAILS_SUCCESS, user});
} catch (error) {
// dispatch a failure action to the store with the error
yield put({ type: types.USER_DETAILS_FAILURE, error });
}
}
export function* watchUserFetchRequest() {
yield takeLatest(types.USER_DETAILS_REQUEST, fetchUserDetails);
}
Reducer is as follows
export default function reducer(state = {}, action = {}) {
switch (action.type) {
case types.USER_DETAILS_SUCCESS:
return {
...state,
user: action.user,
loading: false
};
default:
return state;
}
}
Now i need to set the user details in state so that when the form values are changed, i can call the handleChange function to update the state.
If i had used redux thunk, i could have used something like as follows
componentDidMount() {
this.props.getUserDetails().then(() => {
this.setState({ user });
});
}
so that the user state contains all details of user and if a user property changes then the state can be updated using handleChange method .
That is,
After the api call, i need is something like
state = {
email: user#company.com,
name: 'Ken'
}
How to achieve the same using redux saga?
I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
An error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillUpdate of the parent component but for some reason the catch handler in the middleware from the first code snippet is invoked:
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I do not understand why the catch handler is being invoked as I would have thought the promise has resolved at this point.
Am not completely sure, it's hard to debug by reading code. The obvious answer is because it's all happening within the same stacktrace of the call to next(actionWith({ type: types.SUCCESS, payload: { response } })).
So in this case:
Middleware: Dispatch fetchTeam success inside Promise.then
Redux update props
React: render new props
React: componentWillMount
React: Dispatch new action
If an error occurs at any point, it will bubble up to the Promise.then, which then makes it execute the Promise.catch callback.
Try calling the autocomplete fetch inside a setTimeout to let current stacktrace finish and run the fetch in the next "event loop".
setTimeout(
() => this.props.dispatch(actions.init({ props: this.exportProps() }))
);
If this works, then its' the fact that the event loop hasn't finished processing when the error occurs and from the middleware success dispatch all the way to the autocomplete rendered are function calls after function calls.
NOTE: You should consider using redux-loop, or redux-saga for asynchronous tasks, if you want to keep using your custom middleware maybe you can get some inspiration from the libraries on how to make your api request async from the initial dispatch.