# useSuspense()

High performance async data rendering without overfetching.

`useSuspense()` is like [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) for React components. This means the remainder of the component only runs after the data has loaded, avoiding the complexity of handling loading and error conditions. Instead, fallback handling is
[centralized](https://dataclient.io/docs/getting-started/data-dependency.md#boundaries) with a singular [AsyncBoundary](https://dataclient.io/docs/api/AsyncBoundary.md).

`useSuspense()` is reactive to data [mutations](https://dataclient.io/docs/getting-started/mutations.md); rerendering only when necessary.

## Usage

**Rest**

```typescript title="ProfileResource"
import { Entity, resource } from '@data-client/rest';

export class Profile extends Entity {
  id: number | undefined = undefined;
  avatar = '';
  fullName = '';
  bio = '';

  static key = 'Profile';
}

export const ProfileResource = resource({
  path: '/profiles/:id',
  schema: Profile,
});
```

```tsx title="ProfileDetail"
import { useSuspense } from '@data-client/react';
import { ProfileResource } from './ProfileResource';

function ProfileDetail(): JSX.Element {
  const profile = useSuspense(ProfileResource.get, { id: 1 });
  return (
    <div className="listItem">
      <Avatar src={profile.avatar} />
      <div>
        <h4>{profile.fullName}</h4>
        <p>{profile.bio}</p>
      </div>
    </div>
  );
}
render(<ProfileDetail />);
```

**Promise**

```typescript title="Profile"
import { Endpoint } from '@data-client/endpoint';

export const getProfile = new Endpoint(
  (id: number) =>
    Promise.resolve({
      id,
      fullName: 'Jing Chen',
      bio: 'Creator of Flux Architecture',
      avatar: 'https://avatars.githubusercontent.com/u/5050204?v=4',
    }),
  {
    key(id) {
      return `getProfile${id}`;
    },
  },
);
```

```tsx title="ProfileDetail"
import { useSuspense } from '@data-client/react';
import { getProfile } from './Profile';

function ProfileDetail(): JSX.Element {
  const profile = useSuspense(getProfile, 1);
  return (
    <div className="listItem">
      <Avatar src={profile.avatar} />
      <div>
        <h4>{profile.fullName}</h4>
        <p>{profile.bio}</p>
      </div>
    </div>
  );
}
render(<ProfileDetail />);
```

## Behavior

Cache policy is [Stale-While-Revalidate](https://tools.ietf.org/html/rfc5861) by default but also [configurable](https://dataclient.io/docs/concepts/expiry-policy.md).

| Expiry Status | Fetch           | Suspend | Error             | Conditions                                                                                                                                                                                                                                          |
| ------------- | --------------- | ------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Invalid       | yes<sup>1</sup> | yes     | no                | not in store, [deletion](https://dataclient.io/rest/api/resource.md#delete), [invalidation](https://dataclient.io/docs/api/Controller.md#invalidate), [invalidIfStale](https://dataclient.io/docs/concepts/expiry-policy.md#endpointinvalidifstale) |
| Stale         | yes<sup>1</sup> | no      | no                | (first-render, arg change) & [expiry < now](https://dataclient.io/docs/concepts/expiry-policy.md)                                                                                                                                                   |
| Valid         | no              | no      | maybe<sup>2</sup> | fetch completion                                                                                                                                                                                                                                    |
|               | no              | no      | no                | `null` used as second argument                                                                                                                                                                                                                      |

> **Note**
>
> 1. Identical fetches are automatically deduplicated
> 2. [Hard errors](https://dataclient.io/docs/concepts/error-policy.md#hard) to be [caught](https://dataclient.io/docs/getting-started/data-dependency.md#async-fallbacks) by [Error Boundaries](https://dataclient.io/docs/api/AsyncBoundary.md)

> **Info: React Native**
>
> When using React Navigation, useSuspense() will trigger fetches on focus if the data is considered
> stale.

> **Tip: Conditional Dependencies**
>
> Use `null` as the second argument to any Data Client hook means "do nothing."
>
> ```typescript
> // todo could be undefined if id is undefined
> const todo = useSuspense(TodoResource.get, id ? { id } : null);
> ```

## Types

```typescript
function useSuspense(
  endpoint: ReadEndpoint,
  ...args: Parameters<typeof endpoint> | [null]
): Denormalize<typeof endpoint.schema>;
```

```typescript
function useSuspense<
  E extends EndpointInterface<
    FetchFunction,
    Schema | undefined,
    undefined
  >,
  Args extends readonly [...Parameters<E>] | readonly [null],
>(
  endpoint: E,
  ...args: Args
): E['schema'] extends Exclude<Schema, null>
  ? Denormalize<E['schema']>
  : ReturnType<E>;
```

## Examples

### List

```typescript title="ProfileResource"
import { Entity, resource } from '@data-client/rest';

export class Profile extends Entity {
  id: number | undefined = undefined;
  avatar = '';
  fullName = '';
  bio = '';

  static key = 'Profile';
}

export const ProfileResource = resource({
  path: '/profiles/:id',
  schema: Profile,
});
```

```tsx title="ProfileList"  {5}
import { useSuspense } from '@data-client/react';
import { ProfileResource } from './ProfileResource';

function ProfileList(): JSX.Element {
  const profiles = useSuspense(ProfileResource.getList);
  return (
    <div>
      {profiles.map(profile => (
        <div className="listItem" key={profile.pk()}>
          <Avatar src={profile.avatar} />
          <div>
            <h4>{profile.fullName}</h4>
            <p>{profile.bio}</p>
          </div>
        </div>
      ))}
    </div>
  );
}
render(<ProfileList />);
```

### Pagination

Reactive [pagination](https://dataclient.io/rest/guides/pagination.md) is achieved with [mutable schemas](https://dataclient.io/rest/api/Collection.md)

```ts title="User"
import { Entity } from '@data-client/rest';

export class User extends Entity {
  id = 0;
  name = '';
  username = '';
  email = '';
  phone = '';
  website = '';

  get profileImage() {
    return `https://i.pravatar.cc/64?img=${this.id + 4}`;
  }

  pk() {
    return this.id;
  }
  static key = 'User';
}
```

```ts title="Post" {22,24}
import { Entity, resource, Collection } from '@data-client/rest';
import { User } from './User';

export class Post extends Entity {
  id = 0;
  author = User.fromJS();
  title = '';
  body = '';

  pk() {
    return this.id;
  }
  static key = 'Post';

  static schema = {
    author: User,
  };
}
export const PostResource = resource({
  path: '/posts/:id',
  schema: Post,
  paginationField: 'cursor',
}).extend('getList', {
  schema: { posts: new Collection([Post]), cursor: '' },
});
```

```tsx title="PostItem"
import { type Post } from './Post';

export default function PostItem({ post }: Props) {
  return (
    <div className="listItem spaced">
      <Avatar src={post.author.profileImage} />
      <div>
        <h4>{post.title}</h4>
        <small>by {post.author.name}</small>
      </div>
    </div>
  );
}

interface Props {
  post: Post;
}
```

```tsx title="LoadMore" {7}
import { useController, useLoading } from '@data-client/react';
import { PostResource } from './Post';

export default function LoadMore({ cursor }: { cursor: string }) {
  const ctrl = useController();
  const [loadPage, isPending] = useLoading(
    () => ctrl.fetch(PostResource.getList.getPage, { cursor }),
    [cursor],
  );
  return (
    <center>
      <button onClick={loadPage} disabled={isPending}>
        {isPending ? '...' : 'Load more'}
      </button>
    </center>
  );
}
```

```tsx title="PostList" {7}
import { useSuspense } from '@data-client/react';
import PostItem from './PostItem';
import LoadMore from './LoadMore';
import { PostResource } from './Post';

export default function PostList() {
  const { posts, cursor } = useSuspense(PostResource.getList);
  return (
    <div>
      {posts.map(post => (
        <PostItem key={post.pk()} post={post} />
      ))}
      {cursor ? <LoadMore cursor={cursor} /> : null}
    </div>
  );
}
render(<PostList />);
```

### Sequential

When fetch parameters depend on data from another resource.

```tsx
function PostWithAuthor() {
  const post = useSuspense(PostResource.get, { id });
  const author = useSuspense(UserResource.get, {
    id: post.userId,
  });
}
```

### Conditional

`null` will avoid binding and fetching data

```ts title="Resources"
import { Entity, resource } from '@data-client/rest';

export class Post extends Entity {
  id = 0;
  userId = 0;
  title = '';
  body = '';

  static key = 'Post';
}
export const PostResource = resource({
  path: '/posts/:id',
  schema: Post,
});

export class User extends Entity {
  id = 0;
  name = '';
  username = '';
  email = '';
  phone = '';
  website = '';

  get profileImage() {
    return `https://i.pravatar.cc/64?img=${this.id + 4}`;
  }

  static key = 'User';
}
export const UserResource = resource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/users/:id',
  schema: User,
});
```

```tsx title="PostWithAuthor" {7-11}
import { PostResource, UserResource } from './Resources';

export default function PostWithAuthor({ id }: { id: string }) {
  const post = useSuspense(PostResource.get, { id });
  const author = useSuspense(
    UserResource.get,
    post.userId
      ? {
          id: post.userId,
        }
      : null,
  );
  // author as User | undefined
  if (!author) return;
}
```

### Embedded data

When entities are stored in [nested structures](https://dataclient.io/rest/guides/relational-data.md#nesting), that structure will remain.

```typescript title="api/Post" {12-16}
export class PaginatedPost extends Entity {
  id = '';
  title = '';
  content = '';

  static key = 'PaginatedPost';
}

export const getPosts = new RestEndpoint({
  path: '/post',
  searchParams: { page: '' },
  schema: {
    posts: new Collection([PaginatedPost]),
    nextPage: '',
    lastPage: '',
  },
});
```

```tsx title="ArticleList" {5-7}
import { getPosts } from './api/Post';

export default function ArticleList({ page }: { page: string }) {
  const {
    posts,
    nextPage,
    lastPage,
  } = useSuspense(getPosts, { page });
  return (
    <div>
      {posts.map(post => (
        <div key={post.pk()}>{post.title}</div>
      ))}
    </div>
  );
}
```

### Server Side Rendering

[Server Side Rendering](https://dataclient.io/docs/guides/ssr.md) to incrementally stream HTML,
greatly reducing [TTFB](https://web.dev/ttfb/). [Reactive Data Client SSR's](https://dataclient.io/docs/guides/ssr.md) automatic store hydration
means immediate user interactivity with **zero** client-side fetches on first load.

Example app: [nextjs](https://github.com/reactive/data-client/tree/master/examples/nextjs) ([`resources/TodoResource.ts`](https://github.com/reactive/data-client/blob/master/examples/nextjs/resources/TodoResource.ts), [`components/todo/TodoList.tsx`](https://github.com/reactive/data-client/blob/master/examples/nextjs/components/todo/TodoList.tsx))

Usage in components is identical, which means you can easily share components between SSR and non-SSR
applications, as well as migrate to SSR without needing data-client code changes.

### Concurrent Mode

In React 18 navigating with [startTransition](https://react.dev/reference/react/useTransition#starttransition) allows [AsyncBoundaries](https://dataclient.io/docs/api/AsyncBoundary.md) to
continue showing the previous screen while the new data loads. Combined with
[streaming server side rendering](https://dataclient.io/docs/guides/ssr.md), this eliminates the need to flash annoying
loading indicators - improving the user experience.

Click one of the names to navigate to their todos. Here long loading states are indicated by the
less intrusive _loading bar_, like [YouTube](https://youtube.com) and [Robinhood](https://robinhood.com) use.

Example app: [todo-app](https://github.com/reactive/data-client/tree/master/examples/todo-app) ([`src/pages/Home/TodoList.tsx`](https://github.com/reactive/data-client/blob/master/examples/todo-app/src/pages/Home/TodoList.tsx), [`src/pages/Home/index.tsx`](https://github.com/reactive/data-client/blob/master/examples/todo-app/src/pages/Home/index.tsx), [`src/useNavigationState.ts`](https://github.com/reactive/data-client/blob/master/examples/todo-app/src/useNavigationState.ts))

If you need help adding this to your own custom router, check out the [official React guide](https://react.dev/reference/react/useTransition#building-a-suspense-enabled-router)
