Accessing a reducer state from within another reducer
Asked Answered
C

6

35

I have a reducer whereby I am retuning the appropriate state when an action is dispatched. Now I am calling an API at regular intervals so the result will trigger an action again and again. So what I want is that if the reducer state already has data then another reducer doesn't show the state as loading while the call is sent. It must maintain its loading state when receiving data the first time only. I hope I am able to explain it properly

Here are my code snippets

Loading state reducer

const loading = (state = false, action) => {

    switch (action.type) {
    case 'GET_AUDIT_DATA':         // here I want to return true only when there is no data available
        return true
    case 'GET_AUDIT_DATA_RECEIVED':  
        return false
    case 'GET_AUDIT_DATA_ERROR':
        return false
    default:
        return state
    }
}

Combining reducers

const allReducers = combineReducers({
    auditData: AuditData,
    auditLoading: AuditLoading,
    modifiedOrders: ModifiedOrders
});

export default allReducers;

Reducer returning data on action triggered by superagent

const auditData = (state = [], action) => {
    switch(action.type) {
        case 'GET_AUDIT_DATA_RECEIVED': 
        console.log(action.data);
            return action.data;
        case 'GET_AUDIT_DATA_ERROR': 
            return action.err;
        default :
            return state;
    }
}
export default auditData;

So initially the auditData doesn't contain any data, only after the first success call it returns the data. When this is called at the same time loading state reducer is called and it should return true in GET_AUDIT_DATA action only when the audit data reducer doesn't contain any data.

Also is returning just the current obtained data from auditData the right way to go or I should do it differently. Basically I want to overwrite the current data with the new one.

Cleanse answered 25/10, 2016 at 8:58 Comment(0)
S
9

The best way to proceed is to send to Loading state reducer an information to know if the other reducer already have data. To have at the end:

const loading = (state = false, action) => {

    switch (action.type) {
    case 'GET_AUDIT_DATA':
        if(!action.dataAlreadyInitialized){
            return true
        }
    case 'GET_AUDIT_DATA_RECEIVED':  
        return false
    case 'GET_AUDIT_DATA_ERROR':
        return false
    default:
        return state
    }
}

You should have access from your action function to the application state and do:

dispatch({
  type:'GET_AUDIT_DATA',
  dataAlreadyInitialized: appState.auditData.length > 0
});
Steeve answered 25/10, 2016 at 10:8 Comment(4)
Damien I am dispatching an action from my container, in that case how can I have access to the appStateCleanse
isnt react-redux injecting the appState into your container?Steeve
I am using this via a mapStateToProps function and yeah its working now. ThanksCleanse
Thanks for Help Damien. I would have preferred redux-thunk but then this is really easy to do considering that I was using superagent for my API requests.Cleanse
J
14

You can call getState() over a store to get the list of reducers and the current state inside the reducers.

  1. Import the store into auditLoading (use store to get values. Don't mutate the store)
  2. store.getState().auditLoading will give you the state of auditLoading reducer.

This approach is similar to the callback provided by redux-thunk. In which (dispatch, getState) => {} will be returned to the action.

Johnathanjohnathon answered 25/10, 2016 at 10:38 Comment(1)
in react 16 and redux 4 you can't do this you get => Error: You may not call store.getState() while the reducer is executing. The reducer has already received the state as an argument. Pass it down from the top reducer instead of reading it from the store.Cumbrous
S
9

The best way to proceed is to send to Loading state reducer an information to know if the other reducer already have data. To have at the end:

const loading = (state = false, action) => {

    switch (action.type) {
    case 'GET_AUDIT_DATA':
        if(!action.dataAlreadyInitialized){
            return true
        }
    case 'GET_AUDIT_DATA_RECEIVED':  
        return false
    case 'GET_AUDIT_DATA_ERROR':
        return false
    default:
        return state
    }
}

You should have access from your action function to the application state and do:

dispatch({
  type:'GET_AUDIT_DATA',
  dataAlreadyInitialized: appState.auditData.length > 0
});
Steeve answered 25/10, 2016 at 10:8 Comment(4)
Damien I am dispatching an action from my container, in that case how can I have access to the appStateCleanse
isnt react-redux injecting the appState into your container?Steeve
I am using this via a mapStateToProps function and yeah its working now. ThanksCleanse
Thanks for Help Damien. I would have preferred redux-thunk but then this is really easy to do considering that I was using superagent for my API requests.Cleanse
C
7

The accepted answer is fine (pass in the data length through the action) but can get laborious if it's a piece of information that is widely used. There is another solution that is sometimes preferable for something like 'current user' that might be used by every action.

According to the Redux FAQ https://redux.js.org/faq/reducers it is perfectly acceptable to add a third argument to the reducer function. I.e.:

Loading state reducer

const loading = (state = false, action, noData) => {

    switch (action.type) {
    case 'GET_AUDIT_DATA':
        return noData
    case 'GET_AUDIT_DATA_RECEIVED':  
        return false
    case 'GET_AUDIT_DATA_ERROR':
        return false
    default:
        return state
    }
}

Combining reducers

Unfortunately it means we have to write code to combine the reducers, rather than use the combineReducers shortcut. But it's not too hard, you just call each reducer and create a new object if anything changed:

const allReducers = (state = null, action) => {
    const auditData = AuditData(state?.auditData, action);
    const auditLoading = AuditLoading(state?.auditLoading, action, !state?.auditData?.length);
    const modifiedOrders = ModifiedOrders(state?.modifiedOrders, action);
    return (auditData !== state?.auditData ||
      auditLoading !== state?.auditLoading ||
      modifiedOrders !== state?.modifiedOrders) ?
      { auditData, auditLoading, modifiedOrders } : state;
});

export default allReducers;

Notice the third argument passed to the AuditLoading reducer. No change is required to the other reducers, or to the code that invokes the action. Which is nice!

Collier answered 2/11, 2020 at 3:45 Comment(0)
A
5
import store from '../../../redux/store'

console.log(store.getState().loginReducer.accessToken)

Through this statement we will get state of accessToken

Acanthaceous answered 29/4, 2019 at 6:59 Comment(0)
W
0

You can use Thunk to solve this issue

Let suppose that your action creator looks like this

const getAuditDataAction  = (data) => {
    return { type: "GET_AUDIT_DATA", data }
} 

You will need to add Redux Thunk middleware if you do not have it already

Your action creator can become something like this

const getAuditDataAction  = (data) => {
    return async (dispatch, getState) => {
        const { auditData, auditLoading, modifiedOrders } = getState();
        // Here depending on data you have in your store you 
        // can dispatch the default action or another action 
        // that does something else
        dispatch({ type: "GET_AUDIT_DATA", data })
     }
}
Weinrich answered 18/7, 2023 at 20:17 Comment(0)
S
0

It probably goes without saying that the real right answer is probably "refactor your related reducers into a single larger slice," but sometimes reality doesn't give you the time you need to do that before Feature X has to go out the door.

I took a different approach than what's been said so far here, and here's why:

  • Required no refactoring outside a couple actions and reducers, meaning minimal regression testing
  • Kept my logic in my reducer where it belongs
  • Didn't require any additional packages (only thunk, which I'm already using)
  • Doesn't try to store.getState() from within the reducer, which is illegal in React 16+/Redux 4+
  • Was only gross in ways that were obvious and easily refactorable in the future

Here's the breakdown...

In my action:

dispatch({
  type: 'MY_ACTION',
  payload: myData,
  auxPayload: getState().theOtherImportantSlice // from redux-thunk
})

And my reducer:

const myReducer = (
  state: MyDataShape | null = null,
  action: {
    type: string,
    payload: MyRawDataShape,
    auxPayload: OtherSliceShape | null = null
  }
): MyDataShape | null => {
  if (action.type === 'MY_ACTION') {
    return doThingsWithPayloadBasedOnDataFromAuxPayload(action.payload, action.auxPayload)
  }
  return state
}

And that's really it, pretty straightforward. Now I'm able to make the decisions I need to in my reducer that only possible by cross-referencing existing data. The future will see this refactored out and my store reconfigured to make this cross-talk happen within the slice, but for now this is clean and clear.

Sadi answered 5/7 at 13:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.