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:
connect()
.this.props
in yourrender()
function on the triggered render cycle.As this relates to your component, your
fetchData
function would probably be simplified to something like this: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 fromthis.props
. By doing this, you largely guarantee that the display is up-to-date: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.
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 havegetBalancesCount
to limit the number of instances ofgetBalances
looping at once. Lets walk through how this happens:yield takeLatest(actionTypes.GET_BALANCES.REQUEST, getBalances)
kicks offgetBalances
.getBalances
hitsgetBalanceCount++
, sogetBalanceCount == 1
getBalances
is repeated, due toput({ type: action.type, payload: {} })
getBalances
hitsgetBalanceCount++
, sogetBalanceCount == 2
getBalances
hitsif (getBalanceCount > 1)
, satisfies the condition, decrementsgetBalanceCount
to1
and exits.Now, I'm assuming
yield fork(async, action, API.getBalances...)
eventually dispatchesGET_BALANCES.SUCCESS
inasyncSaga
, so it'll continue to work each time you dispatchGET_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 concurrentgetBalances
running at once. This is already built intotakeLatest
:(See: https://redux-saga.js.org/docs/api/)
So all you really have to do is remove your custom logic:
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: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 aloading
state. In this case, your approach is probably fine.)