Andrew
Andrew

Reputation: 14526

Asynchronous initialization of Redux state

I'm really uncertain how to handle a multi-part, asynchronous initialization process in a redux application. I need to load a bunch of data asynchronously. Only after each piece of data has completed loading can the application be considered "ready". I am really unsure how this is supposed to be done in redux.

Let's say an application needs three things to be completed before it is considered ready. These preconditions are:

  1. theme has retrieved its definition from a local file;
  2. userPrefs has retrieved its settings from local storage; and
  3. api has confirmed that a certain remote server is reachable.

On the one hand, it seems like I can simply add another reducer (app?) that listens for the actions from these three components. When it has heard from all three, the app is "ready". This seems to be the idea behind the advice here.

Alternatively, I can probably derive the data at the consumer. This lookup will probably happen a lot, but I suppose it could be memoized (though it still seems wasteful).

Lastly, I can see myself writing my own combineReducers so that the root reducer has a separate, "top level" branch of the state tree (this just seems like a variation of the first option) or write it so that one reducer has access to the entire state tree.

I think I can write the rest of the application without any cross-cutting concerns, but this doesn't seem to be an unusual situation. How am I supposed to approach this situation?

Upvotes: 0

Views: 3156

Answers (2)

Andrew
Andrew

Reputation: 14526

I've accepted the answer by James J. above, because he took the time to give a completely valid answer and I think his is a great solution. I thought I should include the solution I ended up going with as well, though. There is no one correct answer, but I don't like the idea of putting any business logic in a view layer (it just feels like it should not be there and redux offers other alternatives).

First, here is a boring, top-level, connected component. The only things to notice are the initializeApplication() method prop that is exposed through operators, and the ready state prop (available through the state.application coming from redux.

// components/Application.js
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';

import { operators, selectors } from './store'; // eslint-disable-line no-unused-vars


class Application extends React.Component {
    componentDidMount() {
        this.props.initializeApplication();
    }
    render() {
        return <div>Application ready? { this.props.application.ready }</div>;
    }
}

const mapStateToProps = state => ({
    application: state.application,
});
const mapDispatchToProps = ({
    initializeApplication: operators.initializeApplication,
});

export default connect(mapStateToProps, mapDispatchToProps)(Application);

And here is the redux part of the application. It uses a variation of the ducks structure. In ducks, all actions are exposed through "operators" (basically, types are wrapped by actions, which are wrapped by action creators, which are wrapped by operators).

// store/Application/index.js
import { createReducer } from '../../Utils';
import { operators as UserPrefs } from '../UserPrefs';
import { operators as Api } from '../Api';


// TYPES:
const types = {
    INITIALIZED: 'Application/initialized'
};

// ACTION CREATORS:
const actions = {
    initialized: () => ({ type: INITIALIZED })
};

// OPERATORS:
const initializeApplication = () => dispatch =>
    Promise.all([
        dispatch(UserPrefs.initializeUserPrefs()),
        dispatch(Api.initializeApi())
    ])
    .then(() => dispatch(actions.initialized()));

const operators = {
    initializeApplication
};

// REDUCER:
const initialShape = {
    ready: false
};
const reducer = createReducer(initialShape)({
    [INITIALIZED]: (state) => ({...state, ready: true })
});

// EXPORTS
export default reducer;

export {
    operators,
    types
};

Obviously, the operators being called here must return promises that resolve only after they are done running. For example:

// store/UserPrefs/index.js
// ** NOT INCLUDING REDUCERS, TYPES, ETC **
const initializeUserPrefs = () => dispatch =>
    Promise.all([
        loadDefaultPrefs(),
        loadCurrentPrefs()
    ])
    .then(
        ([
            defaultPrefs,
            currentPrefs
        ]) => dispatch(actions.initialized(defaultPrefs, currentPrefs))
    );

const operators = {
    initializeInventory
};

export {
    operators
};

The only controversial thing here is that some of these ducks-style reducer modules necessarily "know" about other ducks (application knows about userPrefs and api) and dispatches its operators directly, introducing a tight binding, but since redux allows for hierarchical reducer trees, I don't think this is a game-breaker, but I can see why some won't like this solution either.

Upvotes: 2

James
James

Reputation: 3815

Make use of the componentDidMount() lifecycle method to fire off the three action creators which will in turn kick the actions to propagate state changes by the reducers.

This component lifecycle method will make sure that your state tree is ready. Simple as that.

Also, make sure to put the action creators in the top level / parent component, probably App.js since you want to bootstrap these states when the application loads.

I normally separate the reducers into different files - each file denoting a particular state tree. For example, I like to separate authentication related reducers from UI related reducers, and so on. Then you need to use combineReducers as you mentioned.

Upvotes: 2

Related Questions