A better way to represent loading state in ReactJS/Redux apps
nikolay.rocks
nikolay.rocks
This isn't true. Flow is great at enforcing that all potential types/values are handled in an if statement. If the following type was used, the code missing the failure case would have been picked up by Flow:
type Pics = 'fetching' | 'failed' | Array<Pic>
> Option 2: use an object with two props [...] someone accidentally changes the status without changing the data. Or the other way around. This will result in nasty and hard to debug issues where the state and data don't match visually.Again, this can be avoided by using the correct union in Flow, which will pick up right away if the two properties have become inconsistent:
type PicsState =
| { status: 'fetching', list: null }
| { status: 'failed', list: null }
| { status: 'fetched', list: Array<Pic> }For anyone else that this was new to, Flow has documented these as "disjoint unions": https://flow.org/en/docs/types/unions/#toc-disjoint-unions
const picsLoadingReducer = (state = false, action) => {
switch (action.type) {
case PICS_REQUEST: return true;
case PICS_FAILURE: return false;
case PICS_SUCCESS: return false;
default: return state;
}
};
const picsReducer = (state = [], action) => {
switch (action.type) {
case PICS_FAILURE: return action.payload;
case PICS_SUCCESS: return action.payload;
default: return state;
}
};
IMO this is a bit more simple since we don't mix two concepts (am I loading data, what is the data to be rendered).If a refactoring or some other edit of the source leads to that state, the pictures may not get rendered (if the pictures are right but picturesLoading is wrong), or the wrong pictures may get rendered (if picturesLoading is right and pictures is wrong).
The canonical example for that state is pagination. I have thousands of pictures loaded on a server but due to resource constraints the server only responds with 25 pictures. When I scroll to a specific point we make a request to load more pictures. Therefore I end up in the state of pictures being non empty and picturesLoading being true. If I were in charge the UI for that application, I might change the opacity of the container to signal that we are loading in new data. When we get a response from the server we can either append the data to the pictures array or we can replace it with the new data. Depending on how you want to build your product, either choice is valid.
You can tell that 4 states doesn't match up with the article based on how the loading state is rendered - the pictures are ignored in those examples.
Now sure, if you decide you really do need 4 states, then that's a different story and your suggestion works just fine. But that's not a solution to the article's problem.
const fetchPics = () => dispatch => {
dispatch({ type: PICS_REQUEST });
const cached = deserialize(localStorage.get('pics'));
if (cached && cached.length > 0) return dispatch({ type: PICS_SUCCESS, ...});
return api.get('/pics')
...
}The ability to take your whole application state, save it somewhere (e.g. localstorage), then load it back later is one of the cool things enabled by redux, and it also makes it easier to implement things like time-travel debugging.
data Pics
= PicsLoading
| PicsFailed
| PicsLoaded (Array Pic)
Now you only need to figure out how you want to map it into assembler (eh, JavaScript). But that's largely a mechanical task. Like for example a compiler does.Use a object with a tag field (like advocated by redux and well-supported in flow/typescript), or an array where the first element is a numeric tag and the rest payload (that's roughly the bytecode that a Haskell compiler would produce), or, god forbid, classes with instanceof checks or abstract class methods (hello Java).
Fetching data with Redux is kinda broken, though. If a dispatch is done in componentWillMount, a synchronous update like setting a loading flag is not visible in the first render, so most spinners have subtle bugs. Kinda crazy that this common use case is not supported.
> componentWillMount() is invoked immediately before mounting occurs. It is called before render(), therefore setting state synchronously in this method will not trigger a re-rendering. Avoid introducing any side-effects or subscriptions in this method.
> componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request. Setting state in this method will trigger a re-rendering.
But when you use Redux this isn't true! The first render will see the old state of the store even if you did changes to it right befofe. And then you immediately get a new rerender with the correct state. This cannot be fixed with how react-redux is currently implemented, and leads to subtle bugs, especially when unmounting and remounting stuff that tries to clear some state.
Edit: more details https://github.com/reactjs/react-redux/issues/210
If you need the component's state/props to be a certain way for the very first render, use `this.state = {...}` in the contructor, or wait until the Redux store is in the correct shape before mounting the component. (I'm guessing this is what you've done in your HOC, so this sounds like the right solution to me).
Calls like `dispatch` and `setState` shouldn't be expected to reflect in a component's rendering synchronously. And as far as I know, as of Fiber, componentWillMount is even going to become a method that could be called multiple times before the component actually mounts... its usage is pretty widely discouraged for most cases: https://github.com/facebook/react/issues/7671
data State a =
Loading
| Failed
| Loaded [a]
Compared to a representation like `{ loading:true, error: false, pics:[] }`, the above model has the benefit of "making illegal state not representable".One library in js I like to use to model this type of tagged union / ADT values is single-key[1]. Each variant in a tagged union type is represented by an plain object with just one key, for example:
const state1 = { Loading: null };
const state2 = { Failed : <reason> };
const state3 = { Loaded : [...pics] };
The library offers pattern matching syntax on top of this data representation: // render
const reactElement = match(state, {
Loading : () => <Loading />,
Failed : (reason) => <Error {...reason}/>,
Loaded : (pics) => <PicsList pics={pics} />
});
// reducer
const reducer = (state, action) => match(state, {
Loading : () => {
switch (action.type) {
case PICS_FAILURE:
return { Failure: action.payload };
case PICS_SUCCESS:
return { Loaded : action.payload };
default:
return state
}
},
Failed : () => {
switch (action.type) {
case PICS_REQUEST:
return { Loading: true };
default:
return state;
}
},
Loaded : () => {
switch (action.type) {
case PICS_REQUEST:
return { Loading: true };
default:
return state;
}
}
});
[1] https://www.npmjs.com/package/single-keyhttp://blog.jenkster.com/2016/06/how-elm-slays-a-ui-antipatt...
I always use `null` to differentiate between an empty list or string and nothing.
I like when I have a type system that ensures I check for null etc. But I only work on very small projects so this works well in js for me as well.
[] === [] // false
It's more confusing and error prone than other options mentioned in the article. new String[1] == new String[1]
in Java, which would also be false.There's nothing whatsoever illogical about two mutable objects that are not identical being reported as unequal, and it's hard to think of any language that has mutable values at all where there isn't an equivalent feature.
Its common to only get a partial list (e.g. a search, or paginated) or read/update just one item. I expected this "better way" to accomodate for that. The oldest "flux" examples already did.