How to handle erros with redux saga - reactjs

I try to error handling with redux saga. Far now its always returning 404 error when i changed the api url. I organized my reducer and redux saga. but ı can't reach to fetching item
This is my Reducer
const homeReducer = (state : Object = initialState, action : Object) => {
switch (action.type) {
case HOME.GET_MOVIES_START:
return {
...state,
ProgramsLoading: true,
fetching: true
};
case HOME.GET_MOVIES_FINISH:
return {
...state,
ProgramsLoading: false,
programs: action.programs,
fetching: true
};
case HOME.GET_MOVIES_REJECTED:
return {
...state,
ProgramsLoading: false,
fetching: false
};
default:
return state;
}
};
And It's my Redux Saga. There is call api with axios. And manipulate to data in there
function* getPrograms() {
const { data } = yield call(axios.get, "http://localhost:3000/entries");
const fetching = false;
const defaultValue = {
SeriesFilteredData: [],
MoviesFilteredData: []
};
const reducerFunction = (accumulator, currentValue) => {
if (currentValue.releaseYear < 2010) {
return accumulator;
}
if (currentValue.programType === "series") {
accumulator.SeriesFilteredData.push(currentValue);
}
else if (currentValue.programType === "movie") {
accumulator.MoviesFilteredData.push(currentValue);
}
return accumulator;
};
const results = data.reduce(reducerFunction, defaultValue);
if (results) {
yield put(homeActions.getProgramsFinish(results));
}
else {
yield put(homeActions.getProgramsRejected({ ProgramsLoading: false }));
}
}
function* homeFlow() {
console.log(getPrograms, "getPrograms");
yield call(delay, 2000);
yield call(getPrograms);
}
export default function* homeSaga() {
yield takeEvery(HOME.GET_MOVIES_START, homeFlow);
}
How Can I handle to any errors ?

Place all your getPrograms function in a try catch and trigger (put) the homeActions.getProgramsRejected({ ProgramsLoading: false }) action in the catch block
function* getPrograms() {
try {
const { data } = yield call(axios.get, "http://localhost:3000/entries");
// ... the rest of your code
if (results) {
yield put(homeActions.getProgramsFinish(results));
}
else {
yield put(homeActions.getProgramsRejected({ ProgramsLoading: false }));
}
}
catch(error) {
yield put(homeActions.getProgramsRejected({ ProgramsLoading: false }));
}
}
Then you can improve it avoiding to write two identical yield put(homeActions.getProgramsRejected({ ProgramsLoading: false })); calls. Take a look
function* getPrograms() {
try {
const { data } = yield call(axios.get, "http://localhost:3000/entries");
// ... the rest of your code
if (results) {
yield put(homeActions.getProgramsFinish(results));
}
else {
throw new Error('No results');
}
}
catch(error) {
yield put(homeActions.getProgramsRejected({ ProgramsLoading: false }));
}
}
if the axios call fails the catch block puts the homeActions.getProgramsRejected action
if it doesn't fail but there aren't results (your initial management) it throws a new error and, again, the catch block puts the homeActions.getProgramsRejected action
Let me know if it helps you 😉

Related

useEffect not firing on chained dispatch (chained yield put from redux-saga)

Libraries used: redux and redux-saga.
On one of my components I have the useEffect hook that has the dependency set to the status of an API request. That useEffect is then triggering a toast notification based on whether the API request is successful or failed.
But, this useEffect is not firing on each yield put called by the redux-saga defined below.
My intention is the following:
Fire the useEffect in the component after the yield put requestSuccessful(...) or yield put requestFailed(...)
After that, fire the useEffect once again after the yield put initResponseFlags(...)
What's happening currently is that my useEffect is not called at all. I'm assuming it's probably because the yield(s) inside the saga change the redux state so fast (from false to true and then false again) that useEffect is not being able to catch the change.
Places.js (component)
const { places, message, waiting, success, error } = useSelector(state => ({
places: state.Places.data.places,
message: state.Places.requestStatus.message,
waiting: state.Places.requestStatus.waiting,
success: state.Places.requestStatus.success,
error: state.Places.requestStatus.error,
}));
...
useEffect(() => {
console.log("useEffect called");
if (success || error) {
// ... toast message ...
console.log("Firing");
}
}, [dispatch, success, error, message, waiting]);
My project uses the redux-saga library and it contains a generic saga for each API request being made in the app.
saga.js
function* requestStart(args, params) {
if (!params || !params.payload || !params.payload.action)
throw Object.assign(
new Error(`Get action params and payload have not been defined!`),
{ code: 400 }
);
let action = params.payload.action;
let actionPayload = params.payload.actionPayload;
let actions = params.payload.actions;
try {
if (process.env.REACT_APP_API_URL) {
if (actions?.beforeStart) {
yield put(putAction(actions?.beforeStart));
}
const { response, error } = yield call(action, actionPayload);
if (response !== undefined) {
yield put(requestSuccessful(args.namespace, response));
if (actions?.onSuccess) {
yield put(putAction(actions?.onSuccess, response));
}
} else {
yield put(requestFailed(args.namespace, error));
if (actions?.onFailure) {
yield put(putAction(actions?.onFailure, error));
}
}
}
} catch (error) {
yield put(requestFailed(args.namespace, error));
if (args.actions?.onFailure) {
yield put(putAction(params.actions?.onFailure, error));
}
} finally {
yield put(initResponseFlags(args.namespace));
}
}
actions.js
export const initResponseFlags = (namespace) => {
return {
type: `${namespace}/${INIT_RESPONSE_FLAGS}`,
};
};
export const requestStart = (namespace, payload) => {
return {
type: `${namespace}/${REQUEST_START}`,
payload: payload,
};
};
export const requestSuccessful = (namespace, payload) => {
return {
type: `${namespace}/${REQUEST_SUCCESSFUL}`,
payload: payload
};
};
export const requestFailed = (namespace, error) => {
return {
type: `${namespace}/${REQUEST_FAILED}`,
payload: error
};
};
reducer.js
import {
INIT_RESPONSE_FLAGS,
REQUEST_START,
REQUEST_SUCCESSFUL,
REQUEST_FAILED,
} from "./actionTypes";
const initialState = {
message: null,
waiting: false,
success: false,
error: false,
};
const RequestStatus = (namespace) => (state = initialState, action) => {
switch (action.type) {
case `${namespace}/${INIT_RESPONSE_FLAGS}`:
state = {
...state,
message: null,
waiting: false,
success: false,
error: false,
};
break;
case `${namespace}/${REQUEST_START}`:
state = {
...state,
waiting: true,
};
break;
case `${namespace}/${REQUEST_SUCCESSFUL}`:
state = {
...state,
waiting: false,
success: true,
error: false,
};
break;
case `${namespace}/${REQUEST_FAILED}`:
state = {
...state,
waiting: false,
message: action.payload,
success: false,
error: true
};
break;
default: break;
}
return state;
};
export default RequestStatus;
EDIT (12.10.2022. 11:08)
I've just inserted a yield delay(1); of 1ms before my yield put(initResponseFlags(...)); and it works now.
Alas, if there's some other solution (cause this seems like it's botched) - any help would be appreciated!
saga.js
function* requestStart(args, params) {
if (!params || !params.payload || !params.payload.action)
throw Object.assign(
new Error(`Get action params and payload have not been defined!`),
{ code: 400 }
);
let action = params.payload.action;
let actionPayload = params.payload.actionPayload;
let actions = params.payload.actions;
try {
if (process.env.REACT_APP_API_URL) {
if (actions?.beforeStart) {
yield put(putAction(actions?.beforeStart));
}
const { response, error } = yield call(action, actionPayload);
if (response !== undefined) {
console.log('kitica');
yield put(requestSuccessful(args.namespace, response));
if (actions?.onSuccess) {
yield put(putAction(actions?.onSuccess, response));
}
} else {
yield put(requestFailed(args.namespace, error));
if (actions?.onFailure) {
yield put(putAction(actions?.onFailure, error));
}
}
}
} catch (error) {
yield put(requestFailed(args.namespace, error));
if (args.actions?.onFailure) {
yield put(putAction(params.actions?.onFailure, error));
}
} finally {
yield delay(1);
yield put(initResponseFlags(args.namespace));
}
}

Who to load dropdown options from API in react JS with typescript and react saga?

Here is my page, Here I want to load brand option from API.
I have written saga attached below:
Action.tsx
export const getBrandsForDropdown = (request: IPagination) => {
return {
type: actions,
payload: request
}
}
Api.tsx
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
console.log("get brand drop down");
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null }).then(
(data) => {
console.log("get brand drop down in ");
return { data, error: null };
},
(error) => {
return { data: null, error };
}
);
};
Reducer.ts
case actions.GET_BRANDS_DROPDOWN_PENDING:
return {
...state,
loading: true,
};
case actions.GET_BRANDS_DROPDOWN_REJECTED:
return {
...state,
loading: false,
};
case actions.GET_BRANDS_DROPDOWN_RESOLVED:
return {
...state,
loading: false,
brandOptions: action.payload,
};
Saga.ts
function* getBrandForDropDownSaga(action: HandleGetBrandsForDropdown) {
yield put(switchGlobalLoader(true));
yield put(pendingViewBrand());
try {
const { data } = yield getBrandsForDropdown();
yield put(resolvedViewBrand(data));
yield put(switchGlobalLoader(false));
} catch (error) {
yield put(switchGlobalLoader(false));
return;
}
}
After this I don't how to call it in my page and get it as a options in brand dropdown
Original Answer: Just Use Thunk
You can do this with redux-saga but I wouldn't recommend it. redux-thunk is a lot easier to use. Thunk is also built in to #reduxjs/toolkit which makes it even easier.
There is no need for an IPagination argument because you are always setting the pagination to {page: 1, limit: 1000}
Try something like this:
import {
createAsyncThunk,
createSlice,
SerializedError
} from "#reduxjs/toolkit";
import { IDropdownOption } from "office-ui-fabric-react";
import client from ???
// thunk action creator
export const fetchBrandsForDropdown = createAsyncThunk(
"fetchBrandsForDropdown",
async (): Promise<IDropdownOption[]> => {
const query = `user/master/brands?page=1&limit=1000`;
return client(query, { body: null });
// don't catch errors here, let them be thrown
}
);
interface State {
brandOptions: {
data: IDropdownOption[];
error: null | SerializedError;
};
// can have other properties
}
const initialState: State = {
brandOptions: {
data: [],
error: null
}
};
const slice = createSlice({
name: "someName",
initialState,
reducers: {
// could add any other case reducers here
},
extraReducers: (builder) =>
builder
// handle the response from your API by updating the state
.addCase(fetchBrandsForDropdown.fulfilled, (state, action) => {
state.brandOptions.data = action.payload;
state.brandOptions.error = null;
})
// handle errors
.addCase(fetchBrandsForDropdown.rejected, (state, action) => {
state.brandOptions.error = action.error;
})
});
export default slice.reducer;
In your component, kill the brandOptions state and access it from Redux. Load the options when the component mounts with a useEffect.
const brandOptions = useSelector((state) => state.brandOptions.data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchBrandsForDropdown());
}, [dispatch]);
CodeSandbox Link
Updated: With Saga
The general idea of how to write the saga is correct in your code.
take the parent asynchronous action.
put a pending action.
call the API to get data.
put a resolved action with the data or a rejected action with an error.
The biggest mistakes that I'm seeing in your saga are:
Catching errors upstream.
Mismatched data types.
Not wrapping API functions in a call effect.
Error Handling
Your brands.api functions are all catching their API errors which means that the Promise will always be resolved. The try/catch in your saga won't have errors to catch.
If you want to catch the errors in the saga then you need to remove the catch from the functions getBrandsForDropdown etc. You can just return the data directly rather than mapping to { result: data, error: null }. So delete the whole then function. I recommend this approach.
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null });
}
If you want to keep the current structure of returning a { result, error } object from all API calls then you need to modify the saga to look for an error in the function return.
function* getBrandForDropDownSaga() {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
const { data, error } = yield call(getBrandsForDropdown);
if (error) {
yield put(rejectedGetBrands(error.message));
} else {
yield put(resolvedGetBrands(data));
}
yield put(switchGlobalLoader(false));
}
Mismatched Data Types
There's some type mismatching in your reducer and state that you need to address. In some places you are using an array IBrand[] and in others you are using an object { results: IBrand[]; totalItems: number; currentPage: string; }. If you add the return type IState to the reducer then you'll see.
There's also a mismatch between a single IBrand and an array. I don't know the exact shape of your API response, but getBrandsForDropdown definitely has an array of brands somewhere. Your saga getBrandForDropDownSaga is dispatching resolvedViewBrand(data) which takes a single IBrand instead of resolvedGetBrands(data) which takes an array IBrand[]. If you add return types to the functions in your brands.api file then you'll see these mistakes highlighted by Typescript.
Don't Repeat Yourself
You can do a lot of combining in your API and your saga between the getBrands and the getBrandsForDropdown. Getting the brands for the dropdown is just a specific case of getBrands where you set certain arguments: { page: 1, limit: 1000 }.
export interface IPagination {
page?: number;
limit?: number;
sort?: "ASC" | "DESC";
column?: string;
}
export const getBrands = async (request: IPagination): Promise<IBrands> => {
const res = await axios.get<IBrands>('/user/master/brands', {
params: request,
});
return res.data;
};
function* coreGetBrandsSaga(request: IPagination) {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
try {
const data = yield call(getBrands, request);
yield put(resolvedGetBrands(data));
} catch (error) {
yield put(rejectedGetBrands(error?.message));
}
yield put(switchGlobalLoader(false));
}
function* getBrandsSaga(action: HandleGetBrands) {
const { sort } = action.payload;
if ( sort ) {
yield put(setSortBrands(sort));
// what about column?
}
const brandsState = yield select((state: AppState) => state.brands);
const request = {
// defaults
page: 1,
limit: brandsState.rowsPerPage,
column: brandsState.column,
// override with action
...action.payload,
}
// the general function can handle the rest
yield coreGetBrandsSaga(request);
}
function* getBrandsForDropDownSaga() {
// handle by the general function, but set certain the request arguments
yield coreGetBrandsSaga({
page: 1,
limit: 1000,
sort: "ASC",
column: "name",
})
}
export default function* brandsSaga() {
yield takeLatest(HANDLE_GET_BRANDS, getBrandsSaga);
yield takeLatest(GET_BRANDS_DROPDOWN, getBrandForDropDownSaga);
...
}
CodeSandbox

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

how to async/await redux-thunk actions?

action.js
export function getLoginStatus() {
return async(dispatch) => {
let token = await getOAuthToken();
let success = await verifyToken(token);
if (success == true) {
dispatch(loginStatus(success));
} else {
console.log("Success: False");
console.log("Token mismatch");
}
return success;
}
}
component.js
componentDidMount() {
this.props.dispatch(splashAction.getLoginStatus())
.then((success) => {
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
});
}
However, when I write component.js code with async/await like below I get this error:
Possible Unhandled Promise Rejection (id: 0): undefined is not a function (evaluating 'this.props.dispatch(splashAction.getLoginStatus())')
component.js
async componentDidMount() {
let success = await this.props.dispatch(splashAction.getLoginStatus());
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
}
How do I await a getLoginStatus() and then execute the rest of the statements?
Everything works quite well when using .then(). I doubt something is missing in my async/await implementation. trying to figure that out.
The Promise approach
export default function createUser(params) {
const request = axios.post('http://www...', params);
return (dispatch) => {
function onSuccess(success) {
dispatch({ type: CREATE_USER, payload: success });
return success;
}
function onError(error) {
dispatch({ type: ERROR_GENERATED, error });
return error;
}
request.then(success => onSuccess, error => onError);
};
}
The async/await approach
export default function createUser(params) {
return async dispatch => {
function onSuccess(success) {
dispatch({ type: CREATE_USER, payload: success });
return success;
}
function onError(error) {
dispatch({ type: ERROR_GENERATED, error });
return error;
}
try {
const success = await axios.post('http://www...', params);
return onSuccess(success);
} catch (error) {
return onError(error);
}
}
}
Referenced from the Medium post explaining Redux with async/await: https://medium.com/#kkomaz/react-to-async-await-553c43f243e2
Remixing Aspen's answer.
import axios from 'axios'
import * as types from './types'
export function fetchUsers () {
return async dispatch => {
try {
const users = await axios
.get(`https://jsonplaceholder.typicode.com/users`)
.then(res => res.data)
dispatch({
type: types.FETCH_USERS,
payload: users,
})
} catch (err) {
dispatch({
type: types.UPDATE_ERRORS,
payload: [
{
code: 735,
message: err.message,
},
],
})
}
}
}
import * as types from '../actions/types'
const initialErrorsState = []
export default (state = initialErrorsState, { type, payload }) => {
switch (type) {
case types.UPDATE_ERRORS:
return payload.map(error => {
return {
code: error.code,
message: error.message,
}
})
default:
return state
}
}
This will allow you to specify an array of errors unique to an action.
Another remix for async await redux/thunk. I just find this a bit more maintainable and readable when coding a Thunk (a function that wraps an expression to delay its evaluation ~ redux-thunk )
actions.js
import axios from 'axios'
export const FETCHING_DATA = 'FETCHING_DATA'
export const SET_SOME_DATA = 'SET_SOME_DATA'
export const myAction = url => {
return dispatch => {
dispatch({
type: FETCHING_DATA,
fetching: true
})
getSomeAsyncData(dispatch, url)
}
}
async function getSomeAsyncData(dispatch, url) {
try {
const data = await axios.get(url).then(res => res.data)
dispatch({
type: SET_SOME_DATA,
data: data
})
} catch (err) {
dispatch({
type: SET_SOME_DATA,
data: null
})
}
dispatch({
type: FETCHING_DATA,
fetching: false
})
}
reducers.js
import { FETCHING_DATA, SET_SOME_DATA } from './actions'
export const fetching = (state = null, action) => {
switch (action.type) {
case FETCHING_DATA:
return action.fetching
default:
return state
}
}
export const data = (state = null, action) => {
switch (action.type) {
case SET_SOME_DATA:
return action.data
default:
return state
}
}
Possible Unhandled Promise Rejection
Seems like you're missing the .catch(error => {}); on your promise. Try this:
componentDidMount() {
this.props.dispatch(splashAction.getLoginStatus())
.then((success) => {
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
})
.catch(err => {
console.error(err.getMessage());
}) ;
}
use dispatch(this.props.splashAction.getLoginStatus()) instead this.props.dispatch(splashAction.getLoginStatus())

Resources