7 ways to control loading state in a React app
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.
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
orvalidated: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.
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.