Search code examples
javascriptreduxreact-reduxredux-thunk

Chaining async with sync actions in Redux for calculating initial state?


Sorry for the long text... I'm trying to figure out the best way to chain some actions which need to wait for each other for calculating the initial state. Probably reselect would help, but I'm having a hard time figuring out how to fit it in. I am using ImmutableJS, but I'll use standard JSON to describe my troubles. Initial state should be like:

{
"book": {
"id": 1,
"title": "THE book",
"notes": [
  1,
  2,
  4
]
 },
 "detailedNotes": [
{
  "id": 1,
  "text": "Just a note",
  "categories": [
    "Vital"
  ]
},
{
  "id": 2,
  "text": "Testing",
  "categories": [
    "Vital"
  ]
},
{
  "id": 4,
  "text": "Doodling",
  "categories": [
    "Relevant"
  ]
}
],
"notesByCategory": [
{
  "Vital": [
    1,
    2
   ]
 },
 {
  "Relevant": [
    4
  ]
 }
 ],
 "activeCategory": "Vital"
}

book and detailedNotes are from a GET call (using fetch). notesByCategory is calculated by my code (at the moment in the reducer...) and I also need to set the initial value for activeCategory as the first calculated category (Vital in the example). Here's the actions.js and reducer.js relevant code:

        //in actions
    export function fetchBook(id) {
        return function (dispatch) {
            return fetch('/api/book/' + id)
                .then(json => {
                        dispatch(receiveBook(json));
                        dispatch(fetchDetailedNotes(json['notes']));
                    }
                );
        }
    }
    export function fetchDetailedNotes(ids) {
        return function (dispatch) {
            return fetch('/api/notes/?ids=' + ids.join())
                .then(json => dispatch(receiveDetailedNotes(json)))
        }
    }
    export function receiveDetailedNotes(detailedNotes) {
        return {
            type: RECEIVE_DETAILED_Notes,
            detailedNotes
        }
    }

    //in reducer
    function detailedNotesReducer(state = List(), action) {
        switch (action.type) {
            case actions.RECEIVE_DETAILED_NOTES:
                return fromJS(action.detailedNotes);
            default:
                return state
        }
    }
    function notesByCategory(state = List(), action) {
        switch (action.type) {
            case actions.RECEIVE_DETAILED_NOTES:
                return extractNotesByCategory(state, action.detailedNotes);
            default:
                return state
        }
    }

There's reducer composition also, I omitted it. Now, I want to set the initial activeCategory to the first category, but I have a problem, I have NO idea where to put this. I have to wait first to have the notesByCategory part of the state calculated. I feel that this calculation should not be maybe in the reducer. Any suggestions?


Solution

  • You could avoid this by not storing derived data in your state, and instead calculate it using selectors (though you don't necessarily have to use reselect for this).

    A selector is really just a function that takes state passed in through mapStateToProps and derives what your container components get as props.

    Your selectors have access to the entirety of your store, so in the selector that returns activeCategory you could have some logic that return either activeCategory (if not null) in the store or some default based on available categories (if null).

    Treating your store as the 'source of truth' and deriving everything you need from there using selectors has the added benefit of preventing inconsistencies since you can have confidence that the store is correct and independent of other data.