# The Reactive Data Client

Reactive Data Client provides safe and performant [client access](https://dataclient.io/vue/api/useSuspense.md) and [mutation](https://dataclient.io/vue/api/Controller.md#fetch) over [remote data protocols](https://www.freecodecamp.org/news/what-is-an-api-in-english-please-b880a3214a82/).
Both pull/fetch ([REST](https://dataclient.io/rest.md) and [GraphQL](https://dataclient.io/graphql.md)) and push/stream ([WebSockets or Server Sent Events](https://dataclient.io/vue/concepts/managers.md#data-stream)) can be used simultaneously.

It has similar goals
to [Relational Databases](https://en.wikipedia.org/wiki/Relational_database)
but for interactive application clients. Because of this, **if your backend uses a [RDBMS](https://en.wikipedia.org/wiki/Relational_database) like [Postgres](https://www.postgresql.org/)
or [MySQL](https://www.mysql.com/) this is a good indication Reactive Data Client might be for you**. Respectively,
just like one might choose [flat files](https://www.techopedia.com/definition/25956/flat-file) over database storage,
sometimes a less powerful client library is sufficient.

This is no small task. To achieve this, Reactive Data Client' design is aimed at **treating remote data like it is
local**. This means component logic should be no more complex than useState and setState.

## Define API {#endpoint}

[Endpoints](https://dataclient.io/vue/getting-started/resource.md) are the _methods_ of your data. At their core they
are simply asynchronous functions. However, they also define anything else relevant to the [API](https://www.freecodecamp.org/news/what-is-an-api-in-english-please-b880a3214a82/)
like [expiry policy](https://dataclient.io/vue/concepts/expiry-policy.md), [data model](https://dataclient.io/vue/concepts/normalization.md), [validation](https://dataclient.io/vue/concepts/validation.md), and [types](https://dataclient.io/rest/api/RestEndpoint.md#typing).

By _decoupling_ endpoint definitions from their usage, we are able to reuse them in many contexts.

- Easy reuse in different **components** eases co-locating data dependencies
- Reuse with different **[composables](https://dataclient.io/vue/api/useSuspense.md)** and **[imperative actions](https://dataclient.io/vue/api/Controller.md)** allows different behaviors with the same endpoint
- Reuse across different **[platforms](https://dataclient.io/vue/getting-started/installation.md)** like Vue web, or even beyond Vue in React, Angular, Svelte, or Node
- Published as **packages** independent of their consumption

Endpoints are extensible and composable, with protocol implementations ([REST](https://dataclient.io/rest.md), [GraphQL](https://dataclient.io/graphql.md), [Websockets+SSE](https://dataclient.io/vue/concepts/managers.md#data-stream))
to get started quickly, extend, and share common patterns.

```ts
import { RestEndpoint } from '@data-client/rest';

const getTodo = new RestEndpoint({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
});
```

```ts
import { GQLEndpoint } from '@data-client/graphql';

const gql = new GQLEndpoint('/');
export const getTodo = gql.query(`
  query GetTodo($id: ID!) {
    todo(id: $id) {
      id
      title
      completed
    }
  }
`);
```

## Co-locate data dependencies

Make your components reusable by binding the data [where you need it](https://dataclient.io/vue/getting-started/data-dependency.md) with the one-line [useSuspense()](https://dataclient.io/vue/api/useSuspense.md). Much like [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await),
[useSuspense()](https://dataclient.io/vue/api/useSuspense.md) guarantees its data once it returns.

```html title="TodoDetail.vue" {6}
<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import { getTodo } from './api/Todo';

  const props = defineProps<{ id: number }>();
  const todo = await useSuspense(getTodo, () => ({ id: props.id }));
</script>

<template>
  <div>{{ todo.title }}</div>
</template>
```

No more prop drilling, or cumbersome external state management. Reactive Data Client guarantees global referential equality,
data safety and performance.

## Handle loading/error

Avoid 100s of loading spinners by placing Vue's built-in [\<Suspense />](https://vuejs.org/guide/built-ins/suspense.html)
around many suspending components. Its `#fallback` slot renders while any descendant is still awaiting data.
Errors are caught with [onErrorCaptured()](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured).

Typically these are placed at or above navigational boundaries like pages, routes or modals.

```html title="App.vue" {7-10,15,20-22}
<script setup lang="ts">
  import { onErrorCaptured, ref } from 'vue';
  import AnotherRoute from './AnotherRoute.vue';
  import TodoDetail from './TodoDetail.vue';

  const error = ref<Error | null>(null);
  onErrorCaptured(err => {
    error.value = err;
    return false;
  });
</script>

<template>
  <div v-if="error">Error: {{ error.message }}</div>
  <Suspense v-else>
    <template #default>
      <AnotherRoute />
      <TodoDetail :id="5" />
    </template>
    <template #fallback>
      <Loading />
    </template>
  </Suspense>
</template>
```

[Non-Suspense fallback handling](https://dataclient.io/vue/getting-started/data-dependency.md#stateful) can also be used for certain
cases.

## Mutations

[Mutations](https://dataclient.io/vue/getting-started/mutations.md) present another case of reuse - this time of our data. This case is even more critical
because it can not just lead to code bloat, but data ingrity, tearing, and general application jankiness.

When we call our mutation method/endpoint, we need to ensure **all** uses of that data are updated.
Otherwise we're stuck with the complexity, performance, and stuttery application jank of attempting
to cascade endpoint refreshes.

### Keep data consistent and fresh {#entities}

[Entities](https://dataclient.io/vue/concepts/normalization.md) define our data model.

This enables a [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) storage pattern, which
prevents 'data tearing' jank and improves performance.

```ts
import { Entity } from '@data-client/rest';

export class Todo extends Entity {
  id = 0;
  userId = 0;
  title = '';
  completed = false;
}
```

```ts
import { GQLEntity } from '@data-client/graphql';

export class Todo extends GQLEntity {
  userId = 0;
  title = '';
  completed = false;
}
```

The [pk()](https://dataclient.io/rest/api/Entity.md#pk) (primary key) method is used to build a lookup table. This is
commonly known as data normalization. To avoid bugs, application jank and performance problems,
it is critical to [choose the right (normalized) state structure](https://react.dev/learn/choosing-the-state-structure).

We can now bind our Entity to both our get endpoint and update endpoint, providing our runtime
data integrity as well as TypeScript definitions.

```ts {6}
import { RestEndpoint } from '@data-client/rest';

const get = new RestEndpoint({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  schema: Todo,
});

const update = getTodo.extend({
  method: 'PUT',
});

export const TodoResource = { get, update };
```

```ts {14,25}
import { GQLEndpoint } from '@data-client/graphql';

const gql = new GQLEndpoint('/');

const get = gql.query(
  `query GetTodo($id: ID!) {
    todo(id: $id) {
      id
      title
      completed
    }
  }
`,
  { todo: Todo },
);

const update = gql.mutation(
  `mutation UpdateTodo($todo: Todo!) {
    updateTodo(todo: $todo) {
      id
      title
      completed
    }
  }`,
  { updateTodo: Todo },
);

export const TodoResource = { get, update };
```

### Tell Vue to update

Just like assigning to a `ref()`, we must make Vue aware of the any mutations so it can rerender.

[Controller](https://dataclient.io/vue/api/Controller.md) provides this functionality in a type-safe manner.
[Controller.fetch()](https://dataclient.io/vue/api/Controller.md#fetch) lets us trigger mutations.

We can [useController](https://dataclient.io/vue/api/useController.md) to access it in Vue components.

```html title="ArticleEdit.vue"
<script setup lang="ts">
  import { useController } from '@data-client/vue';
  import { TodoResource } from './resources/Todo';
  import ArticleForm from './ArticleForm.vue';

  const props = defineProps<{ id: number }>();
  const ctrl = useController();
  const handleSubmit = data =>
    ctrl.fetch(TodoResource.update, { id: props.id }, data);
</script>

<template>
  <ArticleForm @submit="handleSubmit" />
</template>
```

```html title="ArticleEdit.vue"
<script setup lang="ts">
  import { useController } from '@data-client/vue';
  import { TodoResource } from './resources/Todo';
  import ArticleForm from './ArticleForm.vue';

  const props = defineProps<{ id: number }>();
  const ctrl = useController();
  const handleSubmit = data =>
    ctrl.fetch(TodoResource.update, { id: props.id, ...data });
</script>

<template>
  <ArticleForm @submit="handleSubmit" />
</template>
```

<details>

<summary>Tracking imperative loading/error state</summary>

[useLoading()](https://dataclient.io/vue/api/useLoading.md) enhances async functions by tracking their loading and error states.

```html title="ArticleEdit.vue"
<script setup lang="ts">
  import { useController, useLoading } from '@data-client/vue';
  import { TodoResource } from './resources/Todo';
  import ArticleForm from './ArticleForm.vue';

  const props = defineProps<{ id: number }>();
  const ctrl = useController();
  const [handleSubmit, loading, error] = useLoading(data =>
    ctrl.fetch(TodoResource.update, { id: props.id }, data),
  );
</script>

<template>
  <ArticleForm @submit="handleSubmit" :loading="loading" />
</template>
```

</details>

### More data modeling

What if our entity is not the top level item? Here we define the `getList`
endpoint with [new Collection(\[Todo\])](https://dataclient.io/rest/api/Collection.md) as its schema. [Schemas](https://dataclient.io/vue/concepts/normalization.md#schema) tell Reactive Data Client _where_ to find
the Entities. By placing inside a list, Reactive Data Client knows to expect a response
where each item of the list is the entity specified.

```typescript {6}
import { RestEndpoint, Collection } from '@data-client/rest';

// get and update definitions omitted

const getList = new RestEndpoint({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos',
  schema: new Collection([Todo]),
  searchParams: {} as { userId?: string | number } | undefined,
  paginationField: 'page',
});

export default (TodoResource = { getList, get, update });
```

[Schemas](https://dataclient.io/vue/concepts/normalization.md) also automatically infer and enforce the response type, ensuring
the variable `todos` will be typed precisely.

```html title="TodoList.vue" {6}
<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import { TodoResource } from './resources/Todo';
  import TodoListItem from './TodoListItem.vue';

  const todos = await useSuspense(TodoResource.getList);
</script>

<template>
  <div>
    <TodoListItem v-for="todo in todos" :key="todo.pk()" :todo="todo" />
  </div>
</template>
```

Now we've used our data model in three cases - `TodoResource.get`, `TodoResource.getList` and `TodoResource.update`. Data consistency
(as well as referential equality) will be guaranteed between the endpoints, even after mutations occur.

### Organizing Endpoints

At this point we've defined `TodoResource.get`, `TodoResource.getList` and `TodoResource.update`. You might have noticed
that these endpoint definitions share some logic and information. For this reason Reactive Data Client
encourages extracting shared logic among endpoints.

[Resources](https://dataclient.io/rest/api/resource.md) are collections of endpoints that operate on the same data.

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

class Todo extends Entity {
  id = 0;
  userId = 0;
  title = '';
  completed = false;
}

const TodoResource = resource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  schema: Todo,
  searchParams: {} as { userId?: string | number } | undefined,
  paginationField: 'page',
});
```

[Introduction to Resource](https://dataclient.io/vue/getting-started/resource.md)

<details>

<summary>Resource Endpoints</summary>

```typescript
// read
// GET https://jsonplaceholder.typicode.com/todos/5
const todo = await useSuspense(TodoResource.get, { id: 5 });

// GET https://jsonplaceholder.typicode.com/todos
const todos = await useSuspense(TodoResource.getList);

// GET https://jsonplaceholder.typicode.com/todos?userId=1
const todos = await useSuspense(TodoResource.getList, { userId: 1 });

// mutate
const ctrl = useController();

// GET https://jsonplaceholder.typicode.com/todos?userId=1
ctrl.fetch(TodoResource.getList.getPage, { userId: 1, page: 2 });

// POST https://jsonplaceholder.typicode.com/todos
ctrl.fetch(TodoResource.getList.push, { title: 'my todo' });

// POST https://jsonplaceholder.typicode.com/todos?userId=1
ctrl.fetch(TodoResource.getList.push, { userId: 1 }, { title: 'my todo' });

// PUT https://jsonplaceholder.typicode.com/todos/5
ctrl.fetch(TodoResource.update, { id: 5 }, { title: 'my todo' });

// PATCH https://jsonplaceholder.typicode.com/todos/5
ctrl.fetch(TodoResource.partialUpdate, { id: 5 }, { title: 'my todo' });

// DELETE https://jsonplaceholder.typicode.com/todos/5
ctrl.fetch(TodoResource.delete, { id: 5 });
```

</details>

### Zero delay mutations {#optimistic-updates}

[Controller.fetch](https://dataclient.io/vue/api/Controller.md#fetch) call the mutation endpoint, and update Vue based on the response.
The UI still ultimately waits on the fetch completion to update.

For many cases like toggling todo.completed, incrementing an upvote, or dragging and drop
a frame this can be too slow!

We can optionally tell Reactive Data Client to perform the Vue renders immediately. To do this
we'll need to specify _how_.

[getOptimisticResponse](https://dataclient.io/rest/guides/optimistic-updates.md) is just like an updater function. Using [snap](https://dataclient.io/vue/api/Snapshot.md) for access to the store to get the previous
value, as well as the fetch arguments, we return the _expected_ fetch response.

```typescript
const update = new RestEndpoint({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  method: 'PUT',
  schema: Todo,
  getOptimisticResponse(snap, { id }, body) {
    return {
      id,
      ...body,
    };
  },
});
```

Reactive Data Client ensures [data integrity against any possible networking failure or race condition](https://dataclient.io/rest/guides/optimistic-updates.md#optimistic-transforms), so don't
worry about network failures, multiple mutation calls editing the same data, or other common
problems in asynchronous programming.

### Remotely triggered mutations

Sometimes data change is initiated remotely - either due to other users on the site, admins, etc. Declarative
[expiry policy](https://dataclient.io/vue/concepts/expiry-policy.md) controls allow tight control over updates due to fetching.

However, for data that changes frequently (like exchange price tickers, or live conversations) sometimes push-based
protocols are used like Websockets or Server Sent Events. Reactive Data Client has a [powerful middleware layer called Managers](https://dataclient.io/vue/api/Manager.md),
which can be used to [initiate data updates](https://dataclient.io/vue/concepts/managers.md#data-stream) when receiving new data pushed from the server.

<details>

<summary>StreamManager</summary>

```typescript
import type { Manager, Middleware, ActionTypes } from '@data-client/vue';
import { Controller, actionTypes } from '@data-client/vue';
import type { EntityInterface } from '@data-client/rest';

export default class StreamManager implements Manager {
  declare protected evtSource: WebSocket | EventSource;
  declare protected entities: Record<string, EntityInterface>;

  constructor(
    evtSource: WebSocket | EventSource,
    entities: Record<string, EntityInterface>,
  ) {
    this.evtSource = evtSource;
    this.entities = entities;
  }

  middleware: Middleware = controller => {
    this.evtSource.onmessage = event => {
      try {
        const msg: { type: string; args: [any]; data: any } = JSON.parse(
          event.data,
        );
        if (msg.type in this.entities)
          controller.set(this.entities[msg.type], ...msg.args, msg.data);
      } catch (e) {
        console.error('Failed to handle message');
        console.error(e);
      }
    };
    return next => async action => next(action);
  };

  cleanup() {
    this.evtSource.close();
  }
}
```

</details>

If we don't want the full data stream, we can [useSubscription()](https://dataclient.io/vue/api/useSubscription.md) or [useLive()](https://dataclient.io/vue/api/useLive.md)
to ensure we only listen to the data we care about.

Endpoints with [pollFrequency](https://dataclient.io/rest/api/RestEndpoint.md#pollfrequency) allow reusing the existing HTTP endpoints, eliminating
the need for additional websocket or SSE backends.
Polling is globally orchestrated by the [SubscriptionManager](https://dataclient.io/vue/api/SubscriptionManager.md), so even with many
components subscribed Reactive Data Client will never overfetch.

[//]: # "TODO: ## Relational joins and nesting"

## Debugging

Add the Redux DevTools for
[chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en)
or
[firefox extension](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/)

Click the icon to open the [inspector](https://dataclient.io/vue/getting-started/debugging.md), which allows you to observe dispatched actions,
their effect on the cache state as well as current cache state.

## Mock data

Writing [Fixtures](https://dataclient.io/vue/api/Fixtures.md) is a standard format that can be used across all `@data-client/test` helpers as well as your own uses.

**Detail**

```typescript
import type { Fixture } from '@data-client/test';
import { getTodo } from './todo';

const todoDetailFixture: Fixture = {
  endpoint: getTodo,
  args: [{ id: 5 }] as const,
  response: {
    id: 5,
    title: 'Star Reactive Data Client on Github',
    userId: 11,
    completed: false,
  },
};
```

**Update**

```typescript
import type { Fixture } from '@data-client/test';
import { updateTodo } from './todo';

const todoUpdateFixture: Fixture = {
  endpoint: updateTodo,
  args: [{ id: 5 }, { completed: true }] as const,
  response: {
    id: 5,
    title: 'Star Reactive Data Client on Github',
    userId: 11,
    completed: true,
  },
};
```

**404 error**

```typescript
import type { Fixture } from '@data-client/test';
import { getTodo } from './todo';

const todoDetail404Fixture: Fixture = {
  endpoint: getTodo,
  args: [{ id: 9001 }] as const,
  response: { status: 404, response: 'Not found' },
  error: true,
};
```

**Interceptor**

```typescript
import type { Interceptor } from '@data-client/test';

const currentTimeInterceptor: Interceptor = {
  endpoint: new RestEndpoint({
    path: '/api/currentTime/:id',
  }),
  response({ id }) {
    return {
      id,
      updatedAt: new Date().toISOString(),
    };
  },
  delay: () => 150,
};
```

**Interceptor (stateful)**

```typescript
import type { Interceptor } from '@data-client/test';

const incrementInterceptor: Interceptor = {
  endpoint: new RestEndpoint({
    path: '/api/count/increment',
    method: 'POST',
    body: undefined,
  }),
  response() {
    return {
      count: (this.count = this.count + 1),
    };
  },
  delay: () => 150,
};
```

- Mock data with `MockPlugin` from `@data-client/vue/test`
- [Test composables](https://dataclient.io/guides/unit-testing-composables) with `renderDataCompose()`
- [Test components](https://dataclient.io/vue/guides/unit-testing-components.md) with `mountDataClient()` and [mockInitialState()](https://dataclient.io/vue/api/mockInitialState.md)

## Demo

Example app: [vue-todo-app](https://github.com/reactive/data-client/tree/master/examples/vue-todo-app) ([`src/pages/UserTodos.vue`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/pages/UserTodos.vue), [`src/resources/TodoResource.ts`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/resources/TodoResource.ts))

[![Explore on GitHub](https://badgen.net/badge/icon/github?icon=github\&label)](https://github.com/reactive/data-client/tree/master/examples/vue-todo-app)

[More Demos](https://dataclient.io/demos) 
[ Agent Skills](https://skills.sh/reactive/data-client)
