Optimistic UI is not easy...but it can be easier then ever in SvelteKit with Optimistikit!
Warning
This package is meant to be used with Svelte-Kit as the name suggest. Because it uses api that are only present in Svelte-Kit it will not work in your normal svelte project.
Contributions are always welcome!
For the moment there's no code of conduct neither a contributing guideline but if you found a problem or have an idea feel free to open an issue
If you want the fastest way to open a PR try out Codeflow
Install optimistikit with npm
npm install optimistikit@latest
The concept behind optimistikit is quite straightforward. Instead of using the data
props from SvelteKit you can call the function optimistikit
and get back a function to call whenever data changes and an action to apply to all of your forms.
Imagine you have this +page.server.ts
export async function load() {
const comments = await db.select(comments);
return {
comments,
};
}
export const actions = {
async add({ request }) {
const formData = await request.formData();
const new_comment = formData.get('comment');
if (new_comment) {
await db.insert(comments).values({
content: new_comment,
});
}
},
};
and this +page.svelte
<script lang="ts">
const { data } = $props();
</script>
<form method="post" action="?/add">
<input name="comment" />
<button>Add comment</button>
</form>
<ul>
{#each data.comments as comment}
<li>{comment.content}</li>
{/each}
</ul>
if you want to optimistically add the comment using optimistikit
you would need the following updated to +page.svelte
<script lang="ts">
import { optimistikit } from 'optimistikit';
const { data } = $props();
const { enhance, data: optimistic_data } = optimistikit(() => data);
</script>
<form
use:enhance={(data, { formData }) => {
const new_comment = formData.get('comment');
if (new_comment) {
// just mutate `data`
data.comments.push({
content: new_comment,
});
}
}}
method="post"
action="?/add"
>
<input name="comment" />
<button>Add comment</button>
</form>
<ul>
<!-- use `optimistic_data` instead of `data` -->
{#each optimistic_data.comments as comment}
<li>{comment.content}</li>
{/each}
</ul>
Sometimes the resource that you are updating on the server is always the same resource (eg. updating a comment). When that's the case we want to cancel every concurrent request. You can do this by adding an unique data-key
attribute to the form.
export async function load() {
const comments = await db.select(comments);
return {
comments,
};
}
export const actions = {
// other actions
async edit({ request }) {
const formData = await request.formData();
const new_comment = formData.get('comment');
const id = formData.get('id');
if (new_comment && id) {
await db
.update(comments)
.values({
content: new_comment,
})
.where({
id,
});
}
},
};
and this is the +page.svelte
<script lang="ts">
import { optimistikit } from 'optimistikit';
const { data } = $props();
const { enhance, data: optimistic_data } = optimistikit(() => data);
</script>
<!-- rest of the page -->
<ul>
<!-- use `optimistic_data` instead of `data` -->
{#each optimistic_data.comments as comment}
<li>
<form
method="post"
action="?/edit"
data-key="edit-comment-{comment.id}"
use:enhance={(data, { formData }) => {
const new_comment = formData.get('comment');
const id = formData.get('id');
if (new_comment && id) {
const comment = data.comments.find((comment) => comment.id === id);
// just mutate `data`
comment.content = new_comment;
}
}}
>
<input name="id" type="hidden" value={comment.id} />
<input name="comment" value={comment.content} />
<button>Edit</button>
</form>
</li>
{/each}
</ul>
If you have a form in a nested component it can be tedious to pass either data
or the enhance
action around. To solver this problem there's another export from optimistikit
that allows you to grab the action directly
<script lang="ts">
import { get_action } from 'optimistikit';
import type { PageData } from './$types';
const enhance = get_action<PageData>();
</script>
<form
use:enhance={(data) => {
// your logic
}}
>
<!-- your form -->
</form>
The function optimistikit
can optionally receive an object as argument where you can specify two values:
key
: a string that allows you to have different actions/stores in the same route. Most of the times you will probably not need this since specifying a key also means that the updates from the forms will involve only the state returned from that specificoptimistikit
function.enhance
: some libraries like superforms provide a customenhance
function that is different from the one provided by SvelteKit. To allow you to use this libraries together withoptimistikit
you can pass a customenhance
function. It's important for this function to have the same signature as the sveltekit one.
If you are using svelte@4
you really should upgrade to svelte@5
...but if you can't you can use the legacy tag of this library that uses a store and has a slightly different and less ergonomic API.
You can install it like this:
npm i optimistikit@legacy
Check the documentation here!