# useFetch()

Fetch an Endpoint if it is not in cache or stale. Returns a [Ref](https://vuejs.org/api/reactivity-core.html#ref)
holding the fetch promise (with a `resolved` flag). A new fetch is triggered when the arguments change or
the data is [invalidated](https://dataclient.io/vue/api/Controller.md#invalidate). Use it to start fetches early, then read the data
with [useSuspense()](https://dataclient.io/vue/api/useSuspense.md), [useCache()](https://dataclient.io/vue/api/useCache.md) or [useDLE()](https://dataclient.io/vue/api/useDLE.md).

## Usage

### Parallel data loading

`await useSuspense()` runs sequentially in `<script setup>`. Calling `useFetch()` for each endpoint first
starts every fetch in parallel; the following `useSuspense()` calls then reuse the in-flight requests.

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

export class Post extends Entity {
  id = 0;
  title = '';
  body = '';
  static key = 'Post';
}
export const PostResource = resource({
  path: '/posts/:id',
  schema: Post,
});

export class Comment extends Entity {
  id = 0;
  postId = 0;
  author = '';
  text = '';
  static key = 'Comment';
}
export const CommentResource = resource({
  path: '/comments/:id',
  searchParams: {} as { postId: number },
  schema: Comment,
});
```

```html title="PostWithComments.vue" {7-15}
<script setup lang="ts">
  import { useFetch, useSuspense } from '@data-client/vue';
  import { PostResource, CommentResource } from './Resources';

  const props = defineProps<{ id: number }>();

  // Both fetches start in parallel
  useFetch(PostResource.get, () => ({ id: props.id }));
  useFetch(CommentResource.getList, () => ({ postId: props.id }));

  // useSuspense() reads the results — the second fetch
  // is already in-flight while the first one is awaited
  const post = await useSuspense(PostResource.get, () => ({ id: props.id }));
  const comments = await useSuspense(CommentResource.getList, () => ({
    postId: props.id,
  }));
</script>

<template>
  <article>
    <h3>{{ post.title }}</h3>
    <p>{{ post.body }}</p>
    <h4>Comments</h4>
    <div v-for="comment in comments" :key="comment.id" class="listItem">
      <strong>{{ comment.author }}</strong>: {{ comment.text }}
    </div>
  </article>
</template>
```

### Prefetching

`useFetch()` can also be used standalone to ensure resources are available early in a render tree before they are needed.

> **Tip**
>
> Use in combination with a data-binding hook ([useCache()](https://dataclient.io/vue/api/useCache.md), [useSuspense()](https://dataclient.io/vue/api/useSuspense.md), [useDLE()](https://dataclient.io/vue/api/useDLE.md), [useLive()](https://dataclient.io/vue/api/useLive.md))
> in another component.

```html title="MasterPost.vue"
<script setup lang="ts">
  import { useFetch } from '@data-client/vue';
  import { PostResource } from './Resources';

  const props = defineProps<{ id: number }>();
  useFetch(PostResource.get, () => ({ id: props.id }));
  // ...
</script>
```

## Behavior

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

The returned `Ref` is updated with a new promise whenever a fetch is triggered: on argument change,
[invalidation](https://dataclient.io/vue/api/Controller.md#invalidate), or [reset](https://dataclient.io/vue/api/Controller.md#resetEntireStore).

> **Note**
>
> 1. Identical fetches are automatically deduplicated

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

## Types

```typescript
function useFetch(
  endpoint: ReadEndpoint,
  ...args: MaybeRefsOrGetters<Parameters<typeof endpoint>> | [null]
): Readonly<
  Ref<
    | (Promise<Denormalize<typeof endpoint.schema>> & {
        resolved: boolean;
      })
    | 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.

A new fetch is triggered when the arguments change.

## Examples

### Checking fetch status

Use `promise.resolved` to check whether data is still loading:

```html title="MasterPost.vue"
<script setup lang="ts">
  import { useFetch } from '@data-client/vue';
  import { PostResource } from './Resources';

  const props = defineProps<{ id: number }>();
  const promise = useFetch(PostResource.get, () => ({ id: props.id }));
  if (promise.value && !promise.value.resolved) {
    // fetch is in-flight
  }
  // ...
</script>
```
