7 ways to control loading state in a React app

Michael Carter
5 min readApr 14, 2020

--

Like you, I started out my React development journey working on piddly little projects, what dir structure makes sense, why things works and generally getting something cool up with real time state updates in the window (cool!).

Like you, I also started out with experimenting with how to handle “loading” when processing or making API calls.

I welcome you to join me in thinking through a number of ideas found across the web on the topic managing loading in a webApp. Let’s see which may be the “best way” for your solution. One may suit your needs and be adopted universally, but likely a hybrid of a few is the way to go — i’ll leave that up to you.

This isn’t an exhaustive list, more of a thinking piece. Let’s also assume we are pretty knowledgeable in Redux — generally will speak in Redux lingo, most concepts can be abstracted away and adapted to vanilla React or in a React hook. I’m sure this is may be abstracted at a high level to other frameworks too — like the A one, or the V one.

Pick your poison, read on.

A witty way to say “this post is about Loading”
Photo by Jen Theodore on Unsplash

1. Global Loading Boolean

Fastest and lowest cost idea.

Want to make an API call? dispatch(getData(url)) and update a state value totrue and when done return dispatch(getDataDone(data)) to change loading back to false.

I can only see this valuable in small business .com sites where you may want to initialise with a logo/spinner, and do it once then render the site fully when done. Which in itself isn’t very SPA-like.

We can do better than this. Lets go deeper.

2. A loading state per component

Whether extends React.Component or functional components are you poison, we can useState() or this.setState({loading: true}) to achieve the goals at a more granular level — within the components.

A discovery when sharing the idea here is some people may start mixing app state with component state, which should be kept seperate. A good way to approach this method is building your own components and having it resolve the promise internally, perhaps knowing that err.message property is what has validation feedback. For instance:

<EmailInputWithExternalChecks checker={adCheckPromise} />

Let’s make a custom component called EmailInputWithExternalChecks that manages loading/validation state and reads a promise when seeing if an email can be used on signup (or it already exists). checker prop is a Promise which the component can:

  • manage loading styles, etc.
  • show validated:true or validated:false feedback and styling
  • implicitly read what the Progress is from reading the promise’s state

and suddenly the component idea isn’t so bad. An issue comes in when this scales, many things may want this component to disappear, or show loading. Theres no way elsewhere to know this is loading. The promises passed in are in memory and turn difficult to keep track of.

This seems to be a good solution for custom components with a singular use case in mind (that may be reused elsewhere on the site). Again, the state is lost within the component, and you may have to multiply the state checking elsewhere which can get confusing.

3. A loading state per reducer

To achieve this well, its important to pair your reducers with just the right amount of scope. Too little and you’re code begins looking too verbose, too much and you’ll have too many actions managing and updating the loading state of the reducer.

reducer/app.js
state = {loading: false, errMsg: false}
reducer/users.js
state = {loading: false, errMsg: false}
reducer/products.js
state = {loading: false, errMsg: false}
...

For each action, perhaps it has a ACTION_START and a following ACTION_END which turn loading to true then false respectively.

4. Reducer Based loading state for actionType loading

This idea is more of an expansion of #3 where instead of a boolean, we track by the action type. Lets say, our types/user.js exports:

{USER_GET_START, USER_GET_END, UPERMISSIONS_GET_START, UPERMISSIONS_GET_END}

We may want to track both the UPERMISSIONS and the USER action types, their message and codes. So we can manage the loading state of each by managing an object in our Redux cycle. It could look like:

loading: {
USER: {is: true},
UPERMISSIONS: {is: false, msg: "Not logged in", code: 403}
}

Managing state this way gets much more detailed, we can now possibly attempt retries depending on codes and messages — a lot can be done.

5. Global Loading state with reducers actionType tracked

Similar to #4 except we handle this within the reducer based on overall app logic instead of a dedicated reducer. It’s quite similar.

A core benefit of this is you can mapStateToProps with the whole object, or simply a boolean depending on whats required for that scope.

loading: Object.keys(loading).length > 0 //general boolean

or

loading: (loading[KEY_I_CARE_ABOUT].is)?loading[KEY_I_CARE_ABOUT]:false

Which hones in on a specific loading type a component may care about, and returns extra info like msg, orcode (if the schema from #4 is adopted).

Robert S goes into great depth on a version of this style and can be read deeper here (also introduces the ErrorReducer concept too)

6. A dedicated loading reducer handling loading state of each actionType

In all, loading is primarily a bit of UI logic. A lot of the talk about this topic is based on having (or not) reducers for one vertical of logic.

No matter what action is dispatched, all reducers are run and listen to the action. It’s up to you to intercept the action in handleActions({}) and perhaps also combineActions

export default handleActions({[combineActions(loadingStart, apiCallStart)]: (state, action) => ({ ...state, loading: l }),[combineActions(loadingEnd, apiCallEnd)]: (state, action) => ({ ...state, loading: l }), defaultState)

This means that all the other reducers could handle the App state management, and for any new actions that get introduced, you just need to add its function/name to the combineActions([]) call as above.

Sam Aryasa going into this in a fair bit of detail here and also nudges us away from #3. Marko Troskot also is an advocate and you can read more here.

7. A loading HigherOrderComponent (HOC)

Let’s take a redux store and attach it to a HoC for an App, Component or Container (of components).

Now deciding to ‘show’ a spinner is external to the component which can be left to solely render the UI when it has data (not for when it doesn’t). This can be applied at an App level (think, full page spinner) or at a lower level where you may have a spinner in a button. This is quite versatile, though is downstream. If you are using Redux, likely state is managed there and the value can be passed to the HoC. If on vanilla React this could be helpful at component level state.

Farzad YZ wrote a piece on this method here

Overall…

There are many ways to approach this, and depending on your scale of project i’m certain you have implicitly adopted one of these approaches. I believe its important to take into consideration how much is done and avoid overdoing things to lead down an unruly path.

In the end, none are “right”, some are more widely adopted. Be opinionated.

Fight why you think #5 is better that #7, or why they can work together. A key factor is what your team is accepting of and the effort of refactoring, or doing it in a particular way.

I hope this post helps in firing up discussion about a fundamental topic.

--

--