Redux - managing preload state

2020-05-20 07:52发布

问题:

I'm building an application, where I need to preload people and planet data (it's likely that in the future more preload requirements may be added) on launch of the application. I want to have value in the store that represents the global state of the app as loaded: <boolean>. The value would be true only then when the preload requirements people.loaded: true and planet.loaded: true are true. The store would look something like this:

Store
├── loaded: <Boolean>
├── people:
│   ├── loaded: <Boolean>
│   └── items: []
├── planets:
│   ├── loaded: <Boolean>
│   └── items: []

Separate action creators make the needed async requests and dispatch actions which are handled by the People and Planets reducers. As shown below (uses redux-thunk):

actions/index.js

import * as types from '../constants/action-types';
import {getPeople, getPlanets} from '../util/swapi';

export function loadPeople () {
  return (dispatch) => {
    return getPeople()
      .then((people) => dispatch(addPeople(people)));
  };
}

export function loadPlanets () {
  return (dispatch) => {
    return getPlanets()
      .then((planets) => dispatch(addPeople(planets)));
  };
}

export function addPeople (people) {
  return {type: types.ADD_PEOPLE, people};
}

export function addPlanets (planets) {
  return {type: types.ADD_PLANETS, planets};
}

export function initApp () {
  return (dispatch) => {
    loadPeople()(dispatch);
    loadPlanets()(dispatch);
  };
}

../util/swapi handles fetching people and planet data either from LocalStorage or making a request.

initApp() action creator calls other action creators within site.js just before rendering to DOM as shown below:

site.js

import React from 'react';
import {render} from 'react-dom';
import Root from './containers/root';
import configureStore from './store/configure-store';
import {initApp} from './actions';

const store = configureStore();

// preload data
store.dispatch(initApp());

render(
  <Root store={store} />,
  document.querySelector('#root')
);

1. What are the best practices for managing global preload state of the application in Redux?

2. Is having a global loaded state in the store necessary?

3. What would be a scalable way of checking app loaded state in multiple React components? It doesn't seem right to include People and Planet state for containers that just needs to know the global app state and doesn't handle rendering of People or Planets. Also that would be painful to manage when the global loaded state would be needed in multiple containers.


Quoting part of Dan's answer from Redux - multiple stores, why not? question.

Using reducer composition makes it easy to implement "dependent updates" a la waitFor in Flux by writing a reducer manually calling other reducers with additional information and in a specific order.

4. Does Dan by calling other reducers mean calling nested reducers?

回答1:

First, let me correct your example.

Instead of

export function initApp () {
  return (dispatch) => {
    loadPeople()(dispatch);
    loadPlanets()(dispatch);
  };
}

you can (and should) write

export function initApp () {
  return (dispatch) => {
    dispatch(loadPeople());
    dispatch(loadPlanets());
  };
}

You don’t need to pass dispatch as an argument—thunk middleware takes care of this.
Of course technically your code is valid, but I think my suggestion reads easier.


  1. What are the best practices for managing global preload state of the application in Redux?

What you’re doing seems correct. There are no specific best practices.

  1. Is having a global loaded state in the store necessary?

No. As David notes in his answer, you’re better off storing only necessary state.

  1. What would be a scalable way of checking app loaded state in multiple React components? It doesn't seem right to include People and Planet state for containers that just needs to know the global app state and doesn't handle rendering of People or Planets. Also that would be painful to manage when the global loaded state would be needed in multiple containers.

If you’re concerned about duplication, create a “selector” function and place it alongside your reducers:

// Reducer is the default export
export default combineReducers({
  planets,
  people
});

// Selectors are named exports
export function isAllLoaded(state) {
  return state.people.loaded && state.planets.loaded;
}

Now you can import selectors from your components and use them in mapStateToProps function inside any component:

import { isAllLoaded } from '../reducers';

function mapStateToProps(state) {
  return {
    loaded: isAllLoaded(state),
    people: state.people.items,
    planets: state.planet.items
  };
}
  1. Does Dan by calling other reducers mean calling nested reducers?

Yes, reducer composition usually means calling nested reducers.
Please refer to my free Egghead tutorial videos on this topic:

  • Reducer Composition with Arrays
  • Reducer Composition with Objects
  • Reducer Composition with combineReducers()
  • Implementing combineReducers() from Scratch


回答2:

A loaded flag in your store state makes perfect sense.

However you've got too many of them. You've got state.people.loaded, state.planets.loaded as well as state.loaded. The latter is derived from the first two. Your store really shouldn't contain derived state. Either have just the first two or just the latter.

My recommendation would be to keep the first two, i.e. state.people.loaded and state.planets.loaded. Then your connected component can derive an ultimate loaded state. e.g.

function mapStateToProps(state) {
    return {
        loaded: state.people.loaded && state.planets.loaded,
        people: state.people.items,
        planets: state.planet.items
    };
}