How do you update some state with a redux get func

2019-07-29 03:33发布

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

1条回答
聊天终结者
2楼-- · 2019-07-29 04:18

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:

  1. Component dispatches an action to the store.
  2. The action is processed by your sagas and reducers to update the state in the store.
  3. Redux updates the props being provided to the component via connect().
  4. The updated props trigger a re-render of the component.
  5. 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.)

查看更多
登录 后发表回答