# Optimistic comments

Source: https://signy.dev/examples/optimistic-comments/

Show the comment now, ask the server after.

A new comment goes into the resource with `mutate()` before the server answers, marked as pending. When the fake server fails, the comment is taken back out and a toast offers a retry.

## Preview

[Open the page on its own](https://signy.dev/examples/optimistic-comments/demo/index.html.md)

## Code

Save it as an `.html` file and open it in a browser. It loads Signy and Iris CSS from the CDN, so there is nothing to install or build.

**Show the full page**

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="color-scheme" content="light dark">
    <title>Optimistic comments - Signy example</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/iriscss@1/dist/iris.min.css">
    <script src="https://cdn.jsdelivr.net/npm/signy@1/dist/signy.global.min.js"></script>
  </head>
  <body>
    <main class="container" id="app"></main>

    <script>
      const { Component, Signal, application, html, resource, For, Show, Portal, createRef } = Signy;

      // A pretend server. Saving takes a second, and fails when the switch is on.
      let serverId = 3;
      const fetchComments = () =>
        new Promise((resolve) => setTimeout(() => resolve([
          { id: 1, author: "Ada", text: "Signals make this so much simpler." },
          { id: 2, author: "Grace", text: "Agreed. No more re-renders to think about." },
        ]), 500));
      const saveComment = (text, fail) =>
        new Promise((resolve, reject) => setTimeout(() => {
          if (fail) reject(new Error("The comment could not be saved."));
          else resolve({ id: serverId++, author: "You", text });
        }, 1000));

      // An Iris CSS toast is a manual popover, so it is shown once it is in the page
      class Toast extends Component {
        box = createRef();

        setup() {
          this.onMount(() => this.box.current.showPopover());
        }

        render() {
          return html`
            <div ref=${this.box} class="toast danger" popover="manual" role="alert">
              <p>${this.props.message}</p>
              <button class="small" onClick=${this.props.onRetry}>Retry</button>
              <button class="close" aria-label="Close" onClick=${this.props.onClose}></button>
            </div>
          `;
        }
      }

      class Comments extends Component {
        comments = resource(fetchComments);
        fail = new Signal(false);
        failure = new Signal(null);
        draftId = 0;

        // The comment shows at once, marked as pending, and leaves again if saving fails
        async send(text) {
          const draft = { id: `draft-${this.draftId++}`, author: "You", text, pending: true };
          this.failure.value = null;
          this.comments.mutate((list) => [...(list ?? []), draft]);
          try {
            const saved = await saveComment(text, this.fail.peek());
            this.comments.mutate((list) => list.map((comment) => (comment.id === draft.id ? saved : comment)));
          } catch (error) {
            this.comments.mutate((list) => list.filter((comment) => comment.id !== draft.id));
            this.failure.value = { message: error.message, text };
          }
        }

        submit(event) {
          event.preventDefault();
          const form = event.currentTarget;
          const text = form.elements.text.value.trim();
          if (!text) return;
          form.reset();
          this.send(text);
        }

        render() {
          return html`
            <h1>Comments</h1>
            ${Show({
              when: () => this.comments.loading && !this.comments.value,
              children: html`<p aria-busy="true">Loading comments</p>`,
            })}
            <ul>
              ${For({
                each: () => this.comments.value ?? [],
                keyBy: "id",
                children: (comment) => html`
                  <li style=${comment.pending ? "opacity: 0.6" : ""}>
                    <strong>${comment.author}</strong>: ${comment.text}
                    ${comment.pending ? html`<small aria-busy="true">Sending</small>` : ""}
                  </li>
                `,
              })}
            </ul>

            <form onSubmit=${(e) => this.submit(e)}>
              <label for="text">Your comment</label>
              <textarea id="text" name="text" rows="2"></textarea>
              <label>
                <input type="checkbox" role="switch" onChange=${(e) => (this.fail.value = e.currentTarget.checked)} />
                Make the server fail
              </label>
              <button class="primary">Post</button>
            </form>

            ${Show({
              when: () => this.failure.value,
              children: (failure) => Portal({
                children: html`
                  <${Toast}
                    message=${failure.message}
                    onRetry=${() => this.send(failure.text)}
                    onClose=${() => (this.failure.value = null)}
                  />
                `,
              }),
            })}
          `;
        }
      }

      application(Comments).mount("#app");
    </script>
  </body>
</html>

```

## What it uses

- [Resource](https://signy.dev/documentation/1/reactivity/resource/index.html.md) with `mutate()` and `refetch()`
- [Portal](https://signy.dev/documentation/1/templates/portal-and-error-boundary/index.html.md) for the toast
- [For](https://signy.dev/documentation/1/templates/for-and-index/index.html.md) with `keyBy`
