Skip to content

v0.3.0: Observable Inference, Async State Management, Streams

Compare
Choose a tag to compare
@kennyfrc kennyfrc released this 26 Nov 09:57
· 133 commits to master since this release

Release Notes for Cami.js v0.3.0

We are excited to announce the release of Cami.js v0.3.0! This version introduces significant improvements to the developer experience and the reactivity system.

Here's what's new:

Observable Inference

The boilerplate for defining observables has been reduced. You can now directly initialize properties in the class definition, and they will be automatically treated as observables. This makes component state management more straightforward.

Here's how simple a counter now looks like with this change:

<article>
  <h1>Counter</h1>
  <counter-component
  ></counter-component>
</article>
<script src="https://unpkg.com/cami@latest/build/cami.cdn.js"></script>
<script type="module">
  const { html, ReactiveElement } = cami;

  class CounterElement extends ReactiveElement {
    count = 0

    template() {
      return html`
        <button @click=${() => this.count--}>-</button>
        <button @click=${() => this.count++}>+</button>
        <div>Count: ${this.count}</div>
      `;
    }
  }

  customElements.define('counter-component', CounterElement);
</script>

Notice that you don't have to define observables or effects. You can just directly mutate the state, and the component will automatically update its view.

Streamlined State Management

The query and mutation methods have been added to provide a more seamless experience when fetching and updating data. With these methods, you can easily manage asynchronous data with built-in support for caching, stale time, and refetch intervals. This is inspired by React Query.

Here's how a blog component would look like, with optimistic UI.

<article>
  <h1>Blog</h1>
  <blog-component></blog-component>
</article>
<script src="https://unpkg.com/cami@latest/build/cami.cdn.js"></script>
<script type="module">
  const { html, ReactiveElement, http } = cami;

  class BlogComponent extends ReactiveElement {
    posts = this.query({
      queryKey: ["posts"],
      queryFn: () => {
        return fetch("https://jsonplaceholder.typicode.com/posts").then(res => res.json())
      },
      staleTime: 1000 * 60 * 5 // 5 minutes
    })

    //
    // This uses optimistic UI. To disable optimistic UI, remove the onMutate and onError handlers.
    //
    addPost = this.mutation({
      mutationFn: (newPost) => {
        return fetch("https://jsonplaceholder.typicode.com/posts", {
          method: "POST",
          body: JSON.stringify(newPost),
          headers: {
            "Content-type": "application/json; charset=UTF-8"
          }
        }).then(res => res.json())
      },
      onMutate: (newPost) => {
        // Snapshot the previous state
        const previousPosts = this.posts.data;

        // Optimistically update to the new value
        this.posts.update(state => {
          state.data.push({ ...newPost, id: Date.now() });
        });

        // Return the rollback function and the new post
        return {
          rollback: () => {
            this.posts.update(state => {
              state.data = previousPosts;
            });
          },
          optimisticPost: newPost
        };
      },
      onError: (error, newPost, context) => {
        // Rollback to the previous state
        if (context.rollback) {
          context.rollback();
        }
      },
      onSettled: () => {
        // Invalidate the posts query to refetch the true state
        if (!this.addPost.isSettled) {
          this.invalidateQueries(['posts']);
        }
      }
    });

    template() {
      if (this.posts.data) {
        return html`
          <button @click=${() => this.addPost.mutate({
            title: "New Post",
            body: "This is a new post.",
            userId: 1
          })}>Add Post</button>
          <ul>
            ${this.posts.data.slice().reverse().map(post => html`
              <li>
                <h2>${post.title}</h2>
                <p>${post.body}</p>
              </li>
            `)}
          </ul>
        `;
      }

      if (this.posts.status === "loading") {
        return html`<div>Loading...</div>`;
      }

      if (this.posts.status === "error") {
        return html`<div>Error: ${this.posts.error.message}</div>`;
      }

      if (this.addPost.status === "pending") {
        return html`
        ${this.addPost.status === "pending" ? html`
          <li style="opacity: 0.5;">
            Adding new post...
          </li>
        ` : ''}
        <div>Adding post...</div>`;
      }

      if (this.addPost.status === "error") {
        return html`<div>Error: ${this.addPost.error.message}</div>`;
      }
    }
  }

  customElements.define('blog-component', BlogComponent);
</script>