Same redux actions dispatched in quick succession - reactjs

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.

Related

Redux store cannot update

I have a button trigger project create. What I want to do is when I click button, it will create a new project, and if there is no issue with product, it will go to the next step.
The dispatch on the click handler is working, I can see the redux runs the faild reducer if there is error, but the projectData.successStatus inside the handler cannot get the latest value which the the successStatus I want it to be false. It's still the previsous retrive project list success Status. So the nextStep() condition is not working.
Can someone help me find what's wrong?
This is the handler button:
const handleNextButton = useCallback(() => {
if (newProjectName) {
const newProjectWithProjectName = {
...newProject,
projectName: newProjectName,
}
dispatch(createNewProjectReq(newProjectWithProjectName)) // create new project
if (projectData.successStatus) {
nextStep()
}
}
}, [newProjectName, projectData])
On the action, I have request, add, fail:
export const createNewProjectReq = (newProject) => async (dispatch) => {
dispatch({ type: PROJECT_SENDING_REQUEST })
try {
const result = await createNewProject(newProject)
const { project, message } = result.data.data
dispatch({
type: PROJECT_LIST_ADD,
payload: { project, message },
})
} catch (error) {
dispatch({ type: PROJECT_REQUEST_FAIL, payload: error.data.message })
}
}
Reducer switch:
switch (action.type) {
case PROJECT_SENDING_REQUEST:
console.log("PROJECT_SENDING_REQUEST")
return {
...state,
loading: true,
successStatus: false,
}
case PROJECT_LIST_SUCCESS:
console.log("PROJECT_LIST_SUCCESS")
return {
loading: false,
projects: action.payload.projectListGroupByProjectId,
successStatus: true,
message: action.payload.message,
}
case PROJECT_LIST_ADD:
console.log("PROJECT_LIST_ADD")
return {
...state,
loading: false,
projects: [...state.projects, action.payload.project],
successStatus: true,
message: action.payload.message,
}
case PROJECT_REQUEST_FAIL: {
console.log("PROJECT_REQUEST_FAIL")
return {
...state,
loading: false,
successStatus: false,
message: action.payload,
}
}
default:
return state
}
Issues
handleNextButton callback is synchronous code.
Reducer functions are also synchronous code.
State is generally considered const during a render cycle, i.e. it won't ever change in the middle of a render cycle or synchronous code execution.
Because of these reason the projectData state will not have been updated yet, the conditional check happens before the actions are processed.
Solution
Since you really are just interested in the success of the action so you can go to the next step you can return a boolean value from the asynchronous action and await it or Promise-chain from it.
export const createNewProjectReq = (newProject) => async (dispatch) => {
dispatch({ type: PROJECT_SENDING_REQUEST });
try {
const result = await createNewProject(newProject);
const { project, message } = result.data.data;
dispatch({
type: PROJECT_LIST_ADD,
payload: { project, message },
});
return true; // <-- success status
} catch (error) {
dispatch({ type: PROJECT_REQUEST_FAIL, payload: error.data.message });
return false; // <-- failure status
}
}
...
const handleNextButton = useCallback(() => {
if (newProjectName) {
const newProjectWithProjectName = {
...newProject,
projectName: newProjectName,
}
dispatch(createNewProjectReq(newProjectWithProjectName))
.then(success => {
if (success) {
nextStep();
}
});
}
}, [newProjectName, projectData]);
Demo
Simple demo with click handler calling asynchronous function with 50% chance to succeed/fail.

How to wait for complete return value in dispatch with redux-saga?

I am trying to get myLocation(redux state) variable which is used in next dispatch GET_POSTS_REQUEST. But when i tried to put await to get fully return value, it shows error.
index.js
const testFunc = () => {
const { myLocation} = useSelector(state => state.user);
dispatch({
type: MY_LOCATION_REQUEST,
data: {
lat: position.coords.latitude,
long: position.coords.longitude,
},
});
dispatch({
type: GET_POSTS_REQUEST,
data: {
dong: myLocation.dong,
},
});
};
sagas/location.js
function getLocationAPI(locationInfo) {
return axios.post('/location', locationInfo ,{withCredentials: true});
}
function* getLocation(action) {
try {
const result = yield call(getLocationAPI, action.data);
yield put({
type: GET_LOCATION_SUCCESS,
data: result.data,
});
} catch (e) {
yield put({
type: GET_LOCATION_FAILURE,
error: e,
});
}
}
function* watchGetLocation() {
yield takeLatest(GET_LOCATION_REQUEST, getLocation);
}
export default function* locationSaga() {
yield all([
fork(watchGetLocation),
]);
}
I have to use myLocation for next dispatch action in index.js. But, when i tried to put async/await to my dispatch, it didn't work. Is there any solution for this?
put method kinda dispatches too, for example this part
yield put({
type: GET_LOCATION_SUCCESS,
data: result.data,
});
you can always see as
dispatch({
type: GET_LOCATION_SUCCESS,
data: result.data,
});
So you have to "catch" this with reducer, for example
function yourReducer(state = initialState, action) {
switch (action.type) {
case GET_LOCATION_SUCCESS:
return Object.assign({}, state, {
user: Object.assign({},
state.user,
{
myLocation: action.data
}
]
})
default:
return state
}
}
And this will update your state with data returned from api call

How do I avoid using separate _PENDING _FULFILLED and _REJECTED actions with redux thunk?

I am writing my actions and reducers with thunks that dispatch _PENDING, _FULFILLED, and _REJECTED actions. However, I am wanting a better solution to avoid the boilerplate. I am migrating to Typescript which doubles this boilerplate by requiring an interface for each _PENDING, _FULFILLED, and _REJECTED action. It is just getting out of hand. Is there a way to get the same/similar functionality of my code without having three action types per thunk?
localUserReducer.js
const initialState = {
fetching: false,
fetched: false,
user: undefined,
errors: undefined,
};
export default function (state = initialState, action) {
switch (action.type) {
case 'GET_USER_PENDING':
return {
...state,
fetching: true,
};
case 'GET_USER_FULFILLED':
return {
...state,
fetching: false,
fetched: true,
user: action.payload,
};
case 'GET_USER_REJECTED':
return {
...state,
fetching: false,
errors: action.payload,
};
default:
return state;
}
}
localUserActions.js
import axios from 'axios';
export const getUser = () => async (dispatch) => {
dispatch({ type: 'GET_USER_PENDING' });
try {
const { data } = await axios.get('/api/auth/local/current');
dispatch({ type: 'GET_USER_FULFILLED', payload: data });
} catch (err) {
dispatch({ type: 'GET_USER_REJECTED', payload: err.response.data });
}
};
I may have a huge misunderstand of redux-thunk as I am a newbie. I don't understand how I can send _REJECTED actions if I use the implementation of Typescript and redux-thunk documented here: https://redux.js.org/recipes/usage-with-typescript#usage-with-redux-thunk
There is a way to get the similar functionality without having three action types per thunk, but it will have some impact on the rendering logic.
I'd recommend pushing the transient aspect of the async calls down to the data. So rather than marking your actions as _PENDING, _FULFILLED, and _REJECTED, mark your data that way, and have a single action.
localUser.js (new file for the user type)
// Use a discriminated union here to keep inapplicable states isolated
type User =
{ status: 'ABSENT' } |
{ status: 'PENDING' } |
{ status: 'FULLFILLED', data: { fullName: string } } |
{ status: 'REJECTED', error: string };
// a couple of constructors for the fullfilled and rejected data
function dataFulFilled(data: { fullName: string }) {
return ({ status: 'FULLFILLED', data });
}
function dataRejected(error: string) {
return ({ status: 'REJECTED', error });
}
localUserReducer.js
const initialState: { user: User } = { user: { status: 'ABSENT' } };
export default function (state = initialState, action): { user: User } {
switch (action.type) {
case 'USER_CHANGED':
return {
...state,
user: action.payload
};
default:
return state;
}
}
localUserActions.js
import axios from 'axios';
export const getUser = () => async (dispatch) => {
dispatch({ type: 'USER_CHANGED', payload: { status: 'PENDING' } });
try {
const { data } = await axios.get('/api/auth/local/current');
dispatch({ type: 'USER_CHANGED', payload: dataFulFilled(data) });
} catch (err) {
dispatch({ type: 'USER_CHANGED', payload: dataRejected(err.response.data) });
}
};
This will also remove the need for the multiple boolean fields (fetching and fetched) and isolate the various data states from accidental modification.
The changes to the render logic will be necessary, but will likely be an improvement. Rather than combinations of nested if-else statements using the booleans, a single switch can be used to handle the four cases of the data state.
Then you can invoke something like this from your render function...
function userElement(user: User) {
switch (user.status) {
case 'ABSENT':
return <></>;
case 'PENDING':
return <div>Fetching user information...Please be patient...</div>;
case 'FULLFILLED':
return <div>{user.data.fullName}</div>;
case 'REJECTED':
return <h1>The error is: {user.error}</h1>
}
}
I hope that helps. Good luck!

How do you update some state with a redux get function? Values not updating correctly on set

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

Dispatch multiple actions using Redux Thunk and the await/async syntax to track loading

I am currently editing some reducers to be able to track the loading state of axios operations. Most of my async syntax is written in async/await fashion and would like to keep it that way to keep my code organized.
I am not sure how to dispatch two action creators one after the other: the first one to fire off the FETCHING_USER action type and keep track of the reduced isFetching state, while the other one to fire off the actual axios GET request. The code currently looks like this to get the API request:
export const fetchUser = () => async dispatch => {
const res = await axios.get(`${API_URL}/api/current_user`, {
headers: { authorization: localStorage.getItem("token") }
});
dispatch({ type: FETCH_USER, payload: res.data });
};
I am not sure how to dispatch the FETCHING_USER and then fire off the FETCH_USER action.
First you need to modify your reducer to have isFetching statement and requesting and receiving data cases:
const INITIAL_STATE = { isFetching: false, data: [] };
export default(state = INITIAL_STATE, action) => {
switch(action.type) {
case REQUEST_USER: {
return {...state, isFetching: true};
}
case RECEIVE_USER: {
return {...state, isFetching: false, data: action.payload};
}
default: return state;
}
}
Then modify your action to use try/catch statements:
export const fetchUser = () => async dispatch => {
dispatch({ type: REQUEST_USER });
try {
const res = await axios.get(`${API_URL}/api/current_user`, {
headers: { authorization: localStorage.getItem("token") }
});
dispatch({ type: RECEIVE_USER, payload: res.data });
}
catch(e){
//dispatch your error actions types, (notifications, etc...)
}
};
Then in component you can use condition like: isFetching ? //show loader : //show content (data[])

Resources