Stop using loading spinners
Tags: reactnative, skeleton, react, reanimated
Published: 2021-10-10 00:58:24

Skeletons are better than spinners. If you're refreshing data, or fetching more, show a spinner. But a screen with no data feels less empty with a skeleton.

If you follow me on [Twitter](http://twitter.com/fernandotherojo), you know how much I like skeletons. I even added a `Skeleton` component to [Moti](http://moti.fyi), my animation library for React Native (+ Web).

{% twitter 1376729198224609281 %}

## TLDR

Don't do this:

```tsx
if (!artist) return <Spinner />

return <Artist artist={artist} />
```

Instead, let `Artist` handle its own loading state.

This gets _slightly_ more complicated when it comes to a list of items. But I'll cover that at the end.

Whenever you build a component that receives data asynchronously, you should make it aware of its 2 distinct states: loading & data.

---

# Develop a `Skeleton` mental model

If there's one take-away, it's this: **every component with a loading state should render its own placeholder.**

I especially love this tweet from Paco Coursey.

{% twitter 1428057307670323202 %}

Once you have a pretty `<Skeleton />` component, it might seem like your work is done.

For example, with [Moti's Skeleton](http://moti.fyi/skeleton), all you have to do is this:

```jsx
import { Skeleton } from '@motify/skeleton'

const Artist = ({ artist }) => {
	const loading = !artist

	return (
	  <Skeleton show={loading}>
		  <Text>{artist ? artist.name : 'Loading...'}</Text>
      </Skeleton>
	)
}
```

Seems easy enough. So we can just use `Skeleton` whenever a component has a loading state and we're done, right?

Sure. But let's take it a step further and develop a mental model for building reliable components that display data asynchronously.

We want our components to know _definitively_ if they should show a placeholder state. Thankfully, TypeScript makes this easy.

# Adding TypeScript Support

Let's take our `Artist` component, and define its loading states outside of the component.

A naïve implementation might look like this:

```tsx
type ArtistProps = {
  artist: ArtistSchema | null
  loading: boolean
}
```

But this is bad. 

Our types should describe the shape of our React state. 

However, the code above lets impossible scenarios pass a typechecker.

```tsx
if (props.loading && props.artist) {
  // typescript won't fail here, but it should!
}
```

Let's change our code to use a type union, and turn `boolean` into strict options:

```tsx
type ArtistProps =
  | {
      artist: ArtistSchema
      loading: false
    }
  | {
      artist?: never
      loading: true
    }

const Artist = (props) => {
  return (
    <Skeleton show={props.loading}>
      <Text>{!props.loading ? props.artist.name : 'Loading...'}</Text>
    </Skeleton>
  )
}
```

Notice that `ArtistProps` uses `loading: true|false` instead of `boolean`.

Whenever `props.loading` is `true`, TypeScript knows that `artist` isn't there. By setting `artist?: never`, we ensure that the consuming component _can't_ pass the `artist` prop while loading.

# Consuming the `Artist` component

`Artist` receives the `artist` and `loading` props from a parent. What does that parent look like?

```tsx
// this is our type from earlier
type ArtistProps =
  | {
      artist: ArtistSchema
      loading: false
    }
  | {
      artist?: never
      loading: true
    }

// and this is the parent component
const ArtistScreen = () => {
  const artist = useSWR('/artist')

  return (
    <Artist
      {...(artist.data
        ? { artist: artist.data, loading: false }
        : { loading: true })}
    />
  )
}
```

Easy. We now have two mutually-exclusive states for our `Artist`. When it's loading, show the skeleton. When it's not, show the artist.

Now that we offloaded our logic to TypeScript, we get a delightful developer experience with autocomplete.

You can see what it looks like in the video here:

{% twitter 1366912660273590273 %}

# Lists with Placeholders

The principles for a list are similar to those of a single item.

However, a list should account for 3 states: `empty`, `loading`, and `data`.

```tsx
const ArtistsList = () => {
  const artists = useSWR('/artists')

  // pseudo code
  const loading = !artists.data
  const empty = artists.data?.length === 0
  const data = !!artists.data
}
```

There are 3 possible scenarios: 

1. no data loaded yet
2. data loaded with zero artists
3. data loaded with more than zero artists

## Lay out the list logic

```jsx
const ArtistList = () => {
  const artists = useSWR('/artists')

  if (!artists.data) {
    // we still need to make this
    return <ArtistListPlaceholder />
  } else if (artists.data.length === 0) {
    // make this yourself
    return <Empty />
  }

  return artists.map(artist => (
	<Artist artist={artist} key={artist.id} loading={false} />
  )
}
```

The only thing left is to make the `ArtistListPlaceholder` component.

## Create `ArtistListPlaceholder`

We already have an `Artist` component with a potential loading state, so all we need to do is create an array of `Artist` components, and pass `loading={true}`.

```tsx
const ArtistListPlaceholder = () => {
  // you can adjust this number to fit your UI
  const placeholders = new Array(4).fill('')

  return placeholders.map((_, index) => (
    <Artist
      // index is okay as the key here
      key={`skeleton-${index}`}
      loading
    />
  ))
}
```

Our final code for the list looks like this:

```tsx
const ArtistListPlaceholder = () => { 
  const placeholders = new Array(4).fill('')

  return placeholders.map((_, index) => (
    <Artist 
      key={`skeleton-${index}`}
      loading
    />
  ))
}

const ArtistList = () => {
  const artists = useSWR('/artists')

  if (!artists.data) {
    return <ArtistListPlaceholder />
  } else if (artists.data.length === 0) {
    return <Empty />
  }

  return artists.map(artist => (
	<Artist artist={artist} key={artist.id} loading={false} />
  )
}
```

I like to put the placeholder in the same file as the list component. It makes it easier to maintain.

The result is a nice list of skeletons:

{% twitter 1421930108764540934 %}

## Fading the list in and out

In the video above, I fade the placeholder list out before fading in the data. That's thanks to Moti's `AnimatePresence` component:

{% twitter 1386001622325727232 %}

# Bonus TypeScript utility

Since I use skeletons on many components, I made this type utility to generate their props:

```tsx
type Never<T> = Partial<Record<keyof T, never>>

export type LoadingProps<PropsOnceLoaded> =
  | ({ loading: true } & Never<PropsOnceLoaded>)
  | ({ loading: false } & PropsOnceLoaded)
```

This way, you can easily make components like this:

```tsx
type Props = LoadingProps<{ artist: ArtistSchema }>

const Artist = (props: Props) => {
  // ...
}
```

# Terminology

`loading` is often used as a catch-all term to describe fetching initial data, refreshing, and fetching more. If you prefer, you could change the `loading` prop to `placeholder` in the examples above. It's a preference thing. I like `loading`, but I could be convinced that `placeholder` is a better name.

Don't use `empty` interchangeably with `loading`, though, since `empty` means a list has loaded with zero items.

I use "placeholder" and "skeleton" a bit interchangeably. Think of a skeleton as the UI that implements the placeholder state.

# Placeholders with Suspense

When it comes to suspense, structuring components might be a bit different, since the fallback UI lives outside of the component.

Chances are, you'll do something like this:

```tsx
const ArtistWithData = () => {
  const artist = getArtist()

  return <Artist artist={artist} loading={false} />
}

const SuspendedArtist = () => {
  return (
    <Suspense fallback={<Artist loading />}>
      <ArtistWithData />
    </Suspense>
  )
}
```

I can't say definitively until Suspense becomes mainstream for data fetching, but I think this pattern will remain. I haven't actually used Suspense much, so if you have other ideas for laying out placeholder content, let me know.


# Placeholder text

Here is our original `Artist` component:

```tsx
const Artist = (props) => {
  return (
    <Skeleton show={props.loading}>
      <Text>{!props.loading ? props.artist.name : 'Loading...'}</Text>
    </Skeleton>
  )
}
```

Notice that I wrote `Loading...` when we're in a loading state. 

The `Loading...` text will never actually be shown to the user; instead, it's only used to set the width of the skeleton.

Alternatively, you could use a fixed `width`, which should apply whenever `show` is `true`.

```tsx
<Skeleton width={80} show={props.loading}>
  <Text>{props.artist?.name}</Text>
</Skeleton>
```

# Give me your thoughts

That's all. [Follow me (Fernando Rojo) on Twitter](http://twitter.com/fernandotherojo) for more.

{% twitter 1366902697010622466 %}