# Data mutations

Using our [Create, Update, and Delete](https://dataclient.io/docs/concepts/atomic-mutations.md) endpoints with
[Controller.fetch()](https://dataclient.io/docs/api/Controller.md#fetch) reactively updates _all_ appropriate components atomically (at the same time).

[useController()](https://dataclient.io/docs/api/useController.md) gives components access to this global supercharged [setState()](https://react.dev/reference/react/useState#setstate).

[//]: # "TODO: Add create, and delete examples as well (in tabs)"

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

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

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

```tsx title="TodoItem" {7-11,13-15}
import { useController } from '@data-client/react';
import { TodoResource, type Todo } from './TodoResource';

export default function TodoItem({ todo }: { todo: Todo }) {
  const ctrl = useController();
  const handleChange = e =>
    ctrl.fetch(
      TodoResource.partialUpdate,
      { id: todo.id },
      { completed: e.currentTarget.checked },
    );
  const handleDelete = () =>
    ctrl.fetch(TodoResource.delete, {
      id: todo.id,
    });
  return (
    <div className="listItem nogap">
      <label>
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={handleChange}
        />
        {todo.completed ? <s>{todo.title}</s> : todo.title}
      </label>
      <CancelButton onClick={handleDelete} />
    </div>
  );
}
```

```tsx title="CreateTodo" {8-11}
import { useController } from '@data-client/react';
import { TodoResource } from './TodoResource';

export default function CreateTodo({ userId }: { userId: number }) {
  const ctrl = useController();
  const handleKeyDown = async e => {
    if (e.key === 'Enter') {
      ctrl.fetch(TodoResource.getList.push, {
        userId,
        title: e.currentTarget.value,
      });
      e.currentTarget.value = '';
    }
  };
  return (
    <div className="listItem nogap">
      <label>
        <input type="checkbox" name="new" checked={false} disabled />
        <TextInput size="small" onKeyDown={handleKeyDown} />
      </label>
      <CancelButton />
    </div>
  );
}
```

```tsx title="TodoList"
import { useSuspense } from '@data-client/react';
import { TodoResource } from './TodoResource';
import TodoItem from './TodoItem';
import CreateTodo from './CreateTodo';

function TodoList() {
  const userId = 1;
  const todos = useSuspense(TodoResource.getList, { userId });
  return (
    <div>
      {todos.map(todo => (
        <TodoItem key={todo.pk()} todo={todo} />
      ))}
      <CreateTodo userId={userId} />
    </div>
  );
}
render(<TodoList />);
```

Rather than triggering invalidation cascades or using manually written update functions,
Data Client reactively updates appropriate components using the fetch response.

## Optimistic mutations based on previous state {#optimistic-updates}

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

export class Post extends Entity {
  id = 0;
  author = { id: 0 };
  title = '';
  body = '';
  votes = 0;

  static key = 'Post';

  static schema = {
    author: EntityMixin(
      class User {
        id = 0;
      },
    ),
  };

  get img() {
    return `//loremflickr.com/96/72/kitten,cat?lock=${this.id % 16}`;
  }
}
```

```ts title="PostResource" {15-22}
import { resource } from '@data-client/rest';
import { Post } from './Post';

export { Post };

export const PostResource = resource({
  path: '/posts/:id',
  searchParams: {} as { userId?: string | number } | undefined,
  schema: Post,
}).extend('vote', {
  path: '/posts/:id/vote',
  method: 'POST',
  body: undefined,
  schema: Post,
  getOptimisticResponse(snapshot, { id }) {
    const post = snapshot.get(Post, { id });
    if (!post) throw snapshot.abort;
    return {
      id,
      votes: post.votes + 1,
    };
  },
});
```

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

export default function PostItem({ post }: Props) {
  const ctrl = useController();
  const handleVote = () => {
    ctrl.fetch(PostResource.vote, { id: post.id });
  };
  return (
    <div>
      <div className="voteBlock">
        <small className="vote">
          <button className="up" onClick={handleVote}>
            &nbsp;
          </button>
          {post.votes}
        </small>
        <img src={post.img} width="70" height="52" />
      </div>
      <div>
        <h4>{post.title}</h4>
        <p>{post.body}</p>
      </div>
    </div>
  );
}
interface Props {
  post: Post;
}
```

```tsx title="TotalVotes" {11}
import { Query } from '@data-client/rest';
import { useQuery } from '@data-client/react';
import { PostResource } from './PostResource';

const queryTotalVotes = new Query(
  PostResource.getList.schema,
  posts => posts.reduce((total, post) => total + post.votes, 0),
);

export default function TotalVotes({ userId }: Props) {
  const totalVotes = useQuery(queryTotalVotes, { userId });
  return (
    <center>
      <small>{totalVotes} votes total</small>
    </center>
  );
}
interface Props {
  userId: number;
}
```

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

function PostList() {
  const userId = 2;
  const posts = useSuspense(PostResource.getList, { userId });
  return (
    <div>
      {posts.map(post => (
        <PostItem key={post.pk()} post={post} />
      ))}
      <TotalVotes userId={userId} />
    </div>
  );
}
render(<PostList />);
```

[getOptimisticResponse](https://dataclient.io/rest/guides/optimistic-updates.md) is just like [setState with an updater function](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state). [Snapshot](https://dataclient.io/docs/api/Snapshot.md) provides typesafe access to the previous store value,
which we use to return the _expected_ fetch response.

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.

## Tracking mutation loading

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

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

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

  static key = 'Post';

  get img() {
    return `//loremflickr.com/96/72/kitten,cat?lock=${this.id % 16}`;
  }
}
export const PostResource = resource({
  path: '/posts/:id',
  schema: Post,
});
```

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

export default function PostDetail({ id }) {
  const post = useSuspense(PostResource.get, { id });
  return (
    <div>
      <div className="voteBlock">
        <img src={post.img} width="70" height="52" />
      </div>
      <div>
        <h4>{post.title}</h4>
        <p>{post.body}</p>
      </div>
    </div>
  );
}
```

```tsx title="PostForm"
export default function PostForm({ onSubmit, loading, error }) {
  const handleSubmit = e => {
    e.preventDefault();
    const data = new FormData(e.target);
    onSubmit(data);
  };
  return (
    <form onSubmit={handleSubmit}>
      <TextInput
        label="Title"
        name="title"
        defaultValue="My New Post"
        required
      />
      <TextArea name="body" rows={12} label="Body" required>
        After clicking 'save', the button will be disabled until the POST
        is completed. Upon completion the newly created post is displayed
        immediately as Reactive Data Client is able to use the fetch
        response to populate the store.
      </TextArea>
      {error ? (
        <div className="alert alert--danger">{error.message}</div>
      ) : null}
      <div>
        <button type="submit" disabled={loading}>
          {loading ? 'saving...' : 'Save'}
        </button>
      </div>
    </form>
  );
}
```

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

export default function PostCreate({ navigateToPost }) {
  const ctrl = useController();
  const [handleSubmit, loading, error] = useLoading(
    async data => {
      const post = await ctrl.fetch(PostResource.getList.push, data);
      navigateToPost(post.id);
    },
    [ctrl],
  );
  return (
    <PostForm onSubmit={handleSubmit} loading={loading} error={error} />
  );
}
```

```tsx title="Navigation"
import React from 'react';
import PostCreate from './PostCreate';
import PostDetail from './PostDetail';

function Navigation() {
  const [id, setId] = React.useState<undefined | number>(undefined);
  if (id) {
    return (
      <div>
        <PostDetail id={id} />
        <center>
          <button onClick={() => setId(undefined)}>New Post</button>
        </center>
      </div>
    );
  }
  return <PostCreate navigateToPost={setId} />;
}
render(<Navigation />);
```
