Skip to content

Commit

Permalink
update diy suspense, add admonitions
Browse files Browse the repository at this point in the history
  • Loading branch information
loganzartman committed Nov 8, 2023
1 parent 8118ac6 commit 8757789
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 5 deletions.
3 changes: 3 additions & 0 deletions mdx-components.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type {MDXComponents} from 'mdx/types';

import Admonition from '@/lib/components/Admonition';

export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
Admonition,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,27 @@ date: 2023-11-04T12:00:00

**Not familiar with Suspense?** Check out my [two-minute intro-by-example](/blog/suspense-in-2-minutes).

When I started learning the new features in [React 18][react 18], I was pretty interested in [Suspense][react suspense docs].
When I started learning the new features in [React 18][react 18], I was pretty interested in [Suspense][react suspense docs]. Suspense [promises][suspense post] to make "UI loading state a first class declarative concept in React". In other words, you tell React what to show when something is loading. You don't have to repeat yourself for every network request or image or anything else that could be loading.

```tsx
function MyPage() {
return (
<Suspense fallback={<LoadingSpinner />}>
/* If anything in any of these components is loading, we'll see the
loading spinner! */
<MyHeader />
<MyContent />
<MyFooter />
</Suspense>
);
}
```

Even after reading the docs, I was still curious about something: how do you suspend? In other words, what triggers the fallback state in a `<Suspense>` boundary? The docs dance around this part; they don't even define the component that suspends.

I think that the React team [doesn't intend][suspense in data frameworks] for most people to write code that suspends. Instead, they imagine you'll use libraries that are already integrated with Suspense. In that case, all you need to do is handle the fallback states with the `<Suspense>` component. They likely have thought a lot about this and have good reasons for it!
I think that the React team [doesn't intend][suspense in data frameworks] for most people to write code that suspends. Instead, you'll use libraries that are already integrated with Suspense. In that case, all you need to do is handle the fallback states with the `<Suspense>` component. They likely have thought a lot about this and have good reasons for it!

But this isn't super satisfying to me—it feels like I've only seen half the API. So here's a very basic overview of the suspending part of it. Even if this has limited practicality, I think it's helped my mental model to understand the basics of how it works. Keep in mind that Suspense is based on [promises][promise], so you'll need to know how they work first.
But this isn't super satisfying to me. It feels like I've only seen half the API. So here's a very basic overview of the suspending part of it. Even if this isn't important to know most of the time, I think it's helped my mental model to understand the basics of how it works. Keep in mind that Suspense is based on [promises][promise], so you'll need to know how they work first.

## When to suspend

Expand All @@ -32,15 +46,15 @@ const helloPromise = new Promise<string>((resolve) => {
});
```

Without Suspense, we can hook the promise into the React lifecycle using state.
Without Suspense, we can hook the promise into the React lifecycle using [state][react state].

There are three possible states of the promise:

1. Loading (or "pending")
2. Resolved
3. Errored

Let's create a hook that accepts a promise and outputs its state:
Let's create a hook that accepts a promise and maps _its_ state to _React_ state:

```tsx
import {useState, useEffect, useMemo} from 'react';
Expand Down Expand Up @@ -71,6 +85,12 @@ function usePromise<T>(promise: Promise<T>): {
}
```

<Admonition type="note">

I used `useMemo()` here because I'm returning an object: `{loading, result, error}`. Every time this hook renders, the identity of this object changes, and might cause consumers of this hook to rerender unnecessarily. Since it's memoized, it will only change when its contents change.

</Admonition>

Now we can use it in a component:

```tsx
Expand Down Expand Up @@ -214,6 +234,8 @@ function MyComponent({promise}) {

[promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[react 18]: https://react.dev/blog/2022/03/29/react-v18
[suspense post]: https://react.dev/blog/2022/03/29/react-v18#new-suspense-features
[react state]: https://react.dev/learn/state-a-components-memory
[react suspense docs]: https://react.dev/reference/react/Suspense
[suspense in data frameworks]: https://react.dev/blog/2022/03/29/react-v18#suspense-in-data-frameworks
[simple sandbox]: https://codesandbox.io/s/simple-suspense-z2xhgv
Expand Down
36 changes: 36 additions & 0 deletions src/lib/components/Admonition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export type Type = 'note' | 'warning';

const icons: Record<Type, React.ReactNode> = {
note: <div>🛈</div>,
warning: <div></div>,
};

const messages: Record<Type, React.ReactNode> = {
note: <div>Note</div>,
warning: <div>Warning!</div>,
};

const classNames: Record<Type, string> = {
note: 'text-indigo-100 bg-indigo-500/10 ring-indigo-500',
warning: 'text-amber-100 bg-amber-500/10 ring-amber-500',
};

export default function Admonition({
children,
type,
}: {
children: React.ReactNode;
type: Type;
}) {
return (
<div
className={`flex flex-col prose-p:m-0 p-4 rounded-lg ring-1 ${classNames[type]}`}
>
<div className="flex flex-row items-start gap-2 mb-2 font-bold lowercase">
{icons[type]}
{messages[type]}
</div>
<div>{children}</div>
</div>
);
}

0 comments on commit 8757789

Please sign in to comment.