In React we very often load data which is for some time undefined/null or some placeholder value. Now there are many ways to use loading skeletons, but in my project we have the skeletons rather low in the component hierarchy, so the code goes from this:
<div>
<label>Name</label>
<span>{user.firstname}</span>
<span>{user.lastname}</span>
</div>
<div>
<label>Street address</label>
<span>{user.address.street}</span>
</div>
to something like this:
<div>
<label>Name</label>
<span>{isLoading ? <Skeleton/> : user.firstname}</span>
<span>{isLoading ? <Skeleton/> : user.lastname}</span>
</div>
<div>
<label>Street address</label>
<span>{isLoading ? <Skeleton/> : user.address.street}</span>
</div>
so how can we avoid repetition here and drop the isLoading ternaries, but at the same time re-use the exsiting static jsx markup? Even better if we could just keep the original JSX and replace all nested props ie. user.address.street with a skeleton when user is loading.
ES6 proxies to the rescue. Let's write a recursive proxy that in case of user being
nullreturns a recursive proxy. Let's also define aLoadable<T>type which has a isLoading property and returns either a JSX.Element or the original property.so now Loadable is either loading and user.address.street is of type JSX.Element or it's loaded and then it's string or whatever it was in the first place.
Now let's write our recursive proxy (with a bit of ramda help, but you can easily drop the dependency):
I will explain this in a bit but let's say we load a user somehow:
now when user is null the street property renders our
<Skeleton/>and once user is loaded it will show the street name instead. So how does it do it?Basically whenever we encounter a undefined value on the way down, we return an empty Proxy with a get handler, that does the following:
<Skeleton/>return that instead. This basically makes our Proxy object renderable in jsx.There is even the option to use different loading markup, if one so wishes:
<div>{user.isLoading ? 'Loading....' : user.address.street}</div>I hope someone finds this useful. Of course the placeholder call should be probably wrapped into an own hook with
useMemo(() => placeHolder(data, ...), [data])to avoid recreating the proxy all the time.