Blog with Optimistic UI
This example demonstrates optimistic UI with our Mock API. Optimistic UI is a technique where you update the UI optimistically before the server responds. The optimistic UI logic is handled in the onMutate
handler of the addPost
mutation. In the onMutate
handler, you snapshot the previous state, optimistically update the UI, and return a rollback function.
If the server responds with an error, then you invoke the rollback function to rollback the UI to the previous state in the onError
handler. If the server responds with a success, then you invalidate the cache to refetch the true state from the server in the onSettled
handler.
HTML:
<article>
<blog-component></blog-component>
</article>
<script type="module">
const { html, ReactiveElement, http } = cami;
class BlogComponent extends ReactiveElement {
posts = this.query({
queryKey: ["posts"],
queryFn: () => {
return fetch("https://api.camijs.com/posts?_limit=5")
.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://api.camijs.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.addPost.status === "pending") {
return html`
<div>Adding post...</div>`;
}
if (this.addPost.status === "error") {
return html`<div>Error: ${this.addPost.errorDetails.message}</div>`;
}
if (this.posts.data) {
return html`
<button @click=${() => this.addPost.mutate({
title: "New Post, Made Optimistically",
body: "This is a new post created with optimistic UI. This actually won't persist to the server as we're using a Mock API. So once you refresh, this Blog will rollback to the original state. Refreshes are triggered through window tab changes and full page reloads.",
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.errorDetails.message}</div>`;
}
}
}
customElements.define('blog-component', BlogComponent);
</script>