Agent skill
solidstart-optimistic-ui
SolidStart optimistic UI: use useSubmissions to show pending data immediately, combine server data with pending submissions, filter by pending state, handle rollback on errors.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/solidstart-optimistic-ui
Metadata
Additional technical details for this skill
- globs
-
[ "**/routes/**/*", "**/*action*", "**/*mutation*" ]
SKILL.md
SolidStart Optimistic UI
Optimistic UI shows the expected result immediately without waiting for the server response. If the request fails, the UI rolls back to the previous state.
Core Concept
Use useSubmissions to track pending action submissions and combine them with server data to create an optimistic view.
Basic Pattern
import {
action,
createAsync,
query,
useAction,
useSubmissions,
} from "@solidjs/router";
import { For } from "solid-js";
// Server data
const getServerTodos = query(async () => {
"use server";
return todos;
}, "serverTodos");
// Mutation action
const addTodo = action(async (title: string) => {
"use server";
await new Promise(r => setTimeout(r, 2000));
todos.push({ title });
}, "add-todo");
export default function Home() {
const serverTodos = createAsync(() => getServerTodos());
const addTodoAction = useAction(addTodo);
const todoSubmissions = useSubmissions(addTodo);
// Extract pending todos from submissions
const pendingTodos = () =>
todoSubmissions
.filter(submission => submission.pending)
.map(submission => ({
title: submission.input[0] + " (pending)",
}));
// Combine server data with pending items
const todos = () =>
serverTodos() ? [...serverTodos(), ...pendingTodos()] : [];
return (
<main>
<h1>Todos</h1>
<button onClick={() => addTodoAction("Todo 3")}>Add Todo</button>
<For each={todos()}>
{(todo) => <h3>{todo.title}</h3>}
</For>
</main>
);
}
Key Steps
- Get submissions: Use
useSubmissions(action)to track all submissions for an action - Filter pending: Use
.filter(submission => submission.pending)to only show in-flight requests - Map to data shape: Transform
submission.inputinto the same shape as server data - Combine: Merge server data with pending items:
[...serverData, ...pendingItems]
Important Notes
Accessing Submissions Values
useSubmissions returns a reactive array (with a .pending boolean). Use it directly:
// ✅ Correct
const pendingTodos = () =>
todoSubmissions.filter(submission => submission.pending);
// ✅ Also valid
const pendingTodos = () => [...todoSubmissions];
Filter by Pending State
Only include pending submissions to avoid duplicates. Once a submission completes, the server data will include it:
const pendingTodos = () =>
todoSubmissions
.filter(submission => submission.pending) // Only pending ones!
.map(submission => ({ title: submission.input[0] }));
Accessing Action Input
The submission.input array contains the arguments passed to the action:
// Action with single argument
const addTodo = action(async (title: string) => { ... }, "add-todo");
// submission.input[0] is the title
// Action with multiple arguments
const updateTodo = action(async (id: string, title: string) => { ... }, "update-todo");
// submission.input[0] is id, submission.input[1] is title
// Action with FormData
const addPost = action(async (formData: FormData) => { ... }, "add-post");
// submission.input[0] is FormData
const title = submission.input[0].get("title");
Complete Example with FormData
import {
action,
createAsync,
query,
useSubmissions,
} from "@solidjs/router";
import { For } from "solid-js";
const getPosts = query(async () => {
"use server";
return posts;
}, "posts");
const addPost = action(async (formData: FormData) => {
"use server";
const title = formData.get("title") as string;
await new Promise(r => setTimeout(r, 1500));
posts.push({ title });
}, "add-post");
export default function Home() {
const serverPosts = createAsync(() => getPosts());
const postSubmissions = useSubmissions(addPost);
const pendingPosts = () =>
postSubmissions
.filter(submission => submission.pending)
.map(submission => {
const formData = submission.input[0] as FormData;
return {
title: formData.get("title") + " (pending)",
};
});
const posts = () =>
serverPosts() ? [...serverPosts(), ...pendingPosts()] : [];
return (
<main>
<form action={addPost} method="post">
<input name="title" />
<button>Add Post</button>
</form>
<For each={posts()}>
{(post) => <article>{post.title}</article>}
</For>
</main>
);
}
With Multiple Arguments
const updateTodo = action(async (id: string, title: string, completed: boolean) => {
"use server";
await updateTodoInDB(id, { title, completed });
}, "update-todo");
export default function TodoList() {
const todos = createAsync(() => getTodos());
const submissions = useSubmissions(updateTodo);
const pendingUpdates = () =>
submissions
.filter(submission => submission.pending)
.map(submission => ({
id: submission.input[0], // id
title: submission.input[1], // title
completed: submission.input[2], // completed
pending: true,
}));
// Merge: replace existing todos with pending updates, then add new ones
const todosWithPending = () => {
const serverData = todos() || [];
const pending = pendingUpdates();
return serverData.map(todo => {
const pendingUpdate = pending.find(p => p.id === todo.id);
return pendingUpdate || todo;
}).concat(
pending.filter(p => !serverData.some(t => t.id === p.id))
);
};
return <For each={todosWithPending()}>...</For>;
}
Benefits
- Instant feedback: Users see changes immediately
- Better UX: No waiting for network requests
- Automatic cleanup: When submission completes, it's no longer pending, so it disappears from the optimistic list and appears in server data
- No manual state management: Solid's reactivity handles updates automatically
Best Practices
- Filter by pending: Always filter to only show pending submissions to avoid duplicates
- Match data shape: Ensure optimistic items match the shape of server data
- Visual indicators: Consider adding visual indicators (e.g., "(pending)") to distinguish optimistic items
- Handle errors: If an action fails, the submission will no longer be pending, so it automatically disappears from the optimistic list
- Use with server actions: This pattern works best with SolidStart actions, not just client-side state
When to Use
- Adding items to a list
- Updating item properties
- Toggling boolean states (like, favorite, completed)
- Any mutation where you can predict the result
When NOT to Use
- When the server response contains data you can't predict (e.g., generated IDs, timestamps)
- When operations are likely to fail validation
- When immediate consistency is critical
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?