v0.3.0: Observable Inference, Async State Management, Streams
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>