nross83
nross83

Reputation: 532

How to indicate "loading" state for an async action during first render using redux

Say I have a redux module that looks like this:

import fetch from 'isomorphic-fetch';

// CONSTANTS
const LOAD = 'LOAD';
const LOAD_SUCCESS = 'LOAD_SUCCESS';

// REDUCER
const initialState = { loading: false, data: [] };

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case LOAD: return { ...state, loading: true };
    case LOAD_SUCCESS: return { ...state, loading: false, data: action.data };
    default: return state;
  }
}

// ACTION CREATORS
function requestLocations() {
  return { type: LOAD };
}

function receiveLocations(json) {
  return { type: LOAD_SUCCESS, data: json };
}

export function fetchLocations() {
  return function (dispatch) {
    dispatch(requestLocations());

    return fetch('http://myurl')
      .then(response => response.json())
      .then(json => dispatch(receiveLocations(json)));
  };
}

I'm struggling with the loading state on the first render if I make the async call in componentWillMount. Imagine my component looks like this (simplified for brevity):

export default class LocationContainer extends React.Component {
  componentWillMount() {
    fetchLocations(); // already bound and injected via connect.
  }

  render() {
    const { loading, data } = this.props; // injected via connect from reducer above.
    if (loading) {
      return <Spinner />;
    } else {
      return <LocationExplorer locations={ data } />;
    }
  }
}

The problem I run into is that on the first render of LocationContainer, loading is false and data hasn't been fetched yet. In componentWillMount, the LOAD action is fired and a props change of loading being set to true is queued up to happen on the subsequent render. In the meantime, during my first render, LocationExplorer is rendered when I really wanted Spinner instead because loading is still false. I'm wondering how you deal with this without setting a firstRender = true state variable hack.

Upvotes: 5

Views: 6758

Answers (3)

nross83
nross83

Reputation: 532

Besides the accepted answer, one other solution we've been using to great success is adding a complete boolean flag to the reducer and we mark it true if a response has come back at some point. This makes things a bit more explicit in the event the response actually comes back with an empty array. The complete flag lets me know that the response is from the server and not just my initial state.

if(loading && !complete) {
  return <Spinner />;
} else {
  return <Component data={data} />;
}

I figured I would add this just in case it's helpful to others.

Upvotes: 0

dreyescat
dreyescat

Reputation: 13818

One option could be extend your loading condition with your data initial state:

const initialState = { loading: false, data: [] };

When you are loading and your data is empty it means you are in this exact state of waiting for new data to come:

if (loading && data.length === 0) {
  return <Spinner />;
} else {

Also, I usually put my asynchronous calls in componentDidMount instead of componentWillMount.

Upvotes: 7

David L. Walsh
David L. Walsh

Reputation: 24815

Don't make the request in componentWillMount. Instead, do it before you mount the component. e.g.

store.dispatch(fetchLocations());
ReactDOM.render(<App store={store} />, mountpoint);

Upvotes: 0

Related Questions