# useDLE() - \[D]ata \[L]oading \[E]rror

High performance async data rendering without overfetching. With fetch meta data.

In case you cannot use [suspense](https://dataclient.io/vue/getting-started/data-dependency.md#async-fallbacks), useDLE() is just like [useSuspense()](https://dataclient.io/vue/api/useSuspense.md) but returns \[D]ata \[L]oading \[E]rror values.

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

## Usage

```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,
});
```

```html title="ProfileList.vue"
<script setup lang="ts">
  import { useDLE } from '@data-client/vue';
  import { ProfileResource } from './ProfileResource';

  const { data, loading, error } = useDLE(ProfileResource.getList);
</script>

<template>
  <div v-if="error">Error {{ error.status }}</div>
  <Loading v-else-if="loading || !data" />
  <div v-else>
    <div class="listItem" v-for="profile in data" :key="profile.pk()">
      <Avatar :src="profile.avatar" />
      <div>
        <h4>{{ profile.fullName }}</h4>
        <p>{{ profile.bio }}</p>
      </div>
    </div>
  </div>
</template>
```

## Behavior

`data`, `loading` and `error` are each a [ComputedRef](https://vuejs.org/api/reactivity-core.html#computed).
Destructure them at the top level of `<script setup>` so they are unwrapped in the template. The table
below describes their `.value`.

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

> **Note**
>
> 1. Identical fetches are automatically deduplicated
> 2. [Hard errors](https://dataclient.io/vue/concepts/error-policy.md#hard) to be [caught](https://dataclient.io/vue/getting-started/data-dependency.md#async-fallbacks) by [onErrorCaptured()](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured)

> **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 = useDLE(
>   TodoResource.get,
>   computed(() => (id.value ? { id: id.value } : null)),
> );
> ```

## Types

```typescript
function useDLE(
  endpoint: ReadEndpoint,
  ...args: MaybeRefsOrGetters<Parameters<typeof endpoint>> | [null]
): {
  data: ComputedRef<DenormalizeNullable<typeof endpoint.schema>>;
  loading: ComputedRef<boolean>;
  error: ComputedRef<ErrorTypes | undefined>;
};
```

Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter
functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't
follow prop or route changes, so use a getter or `computed` when an argument can change.

The results update when the arguments change.

## Examples

### Detail

```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,
});
```

```html title="ProfileDetail.vue"
<script setup lang="ts">
  import { useDLE } from '@data-client/vue';
  import { ProfileResource } from './ProfileResource';

  const {
    data: profile,
    loading,
    error,
  } = useDLE(ProfileResource.get, { id: 1 });
</script>

<template>
  <div v-if="error">Error {{ error.status }}</div>
  <Loading v-else-if="loading || !profile" />
  <div v-else class="listItem">
    <Avatar :src="profile.avatar" />
    <div>
      <h4>{{ profile.fullName }}</h4>
      <p>{{ profile.bio }}</p>
    </div>
  </div>
</template>
```

### 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,
});
```

```html title="PostWithAuthor.vue" {15-21}
<script setup lang="ts">
  import { computed } from 'vue';
  import { useDLE } from '@data-client/vue';
  import { PostResource, UserResource } from './Resources';

  const props = defineProps<{ id: string }>();
  const {
    data: post,
    loading: postLoading,
    error: postError,
  } = useDLE(PostResource.get, () => ({ id: props.id }));
  const {
    data: author,
    loading: authorLoading,
    error: authorError,
  } = useDLE(
    UserResource.get,
    computed(() =>
      post.value?.userId
        ? {
            id: post.value.userId,
          }
        : null,
    ),
  );
</script>

<template>
  <div v-if="postError">Error {{ postError.status }}</div>
  <Loading v-else-if="postLoading || !post" />
  <div v-else-if="authorError">Error {{ authorError.status }}</div>
  <Loading v-else-if="authorLoading || !author" />
  <div v-else>{{ author.username }}</div>
</template>
```

### 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"
import { Entity, RestEndpoint, Collection } from '@data-client/rest';

export class PaginatedPost extends Entity {
  id = '';
  title = '';
  content = '';

  static key = 'PaginatedPost';
}

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

```html title="ArticleList.vue" {14}
<script setup lang="ts">
  import { useDLE } from '@data-client/vue';
  import { getPosts } from './api/Post';

  const props = defineProps<{ page: string }>();
  const { data, loading, error } = useDLE(getPosts, () => ({ page: props.page }));
</script>

<template>
  <div v-if="error">Error {{ error.status }}</div>
  <Loading v-else-if="loading || !data" />
  <div v-else>
    <div v-for="post in data.results" :key="post.pk()">
      {{ post.title }}
    </div>
  </div>
</template>
```
