Data mutation
Edit this pageThis guide provides practical examples of using actions to mutate data in SolidStart.
Handling form submission
To handle <form> submissions with an action:
- Ensure the action has a unique name. See the Action API reference for more information.
- Pass the action to the
<form>element using theactionprop. - Ensure the
<form>element uses thepostmethod for submission. - Use the
FormDataobject in the action to extract field data using the naviteFormDatamethods.
// src/routes/index.tsximport { action } from "@solidjs/router";
const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { return ( <form action={addPost} method="post"> <input name="title" /> <button>Add Post</button> </form> );}// src/routes/index.jsximport { action } from "@solidjs/router";
const addPost = action(async (formData) => { const title = formData.get("title"); await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { return ( <form action={addPost} method="post"> <input name="title" /> <button>Add Post</button> </form> );}Passing additional arguments
To pass additional arguments to your action, use the with method:
// src/routes/index.tsximport { action } from "@solidjs/router";
const addPost = action(async (userId: number, formData: FormData) => { const title = formData.get("title") as string; await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ userId, title }), });}, "addPost");
export default function Page() { const userId = 1; return ( <form action={addPost.with(userId)} method="post"> <input name="title" /> <button>Add Post</button> </form> );}// src/routes/index.jsximport { action } from "@solidjs/router";
const addPost = action(async (userId, formData) => { const title = formData.get("title"); await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ userId, title }), });}, "addPost");
export default function Page() { const userId = 1; return ( <form action={addPost.with(userId)} method="post"> <input name="title" /> <button>Add Post</button> </form> );}Showing pending UI
To display a pending UI during action execution:
- Import
useSubmissionfrom@solidjs/router. - Call
useSubmissionwith your action, and use the returnedpendingproperty to display pending UI.
// src/routes/index.tsximport { action, useSubmission } from "@solidjs/router";
const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { const submission = useSubmission(addPost); return ( <form action={addPost} method="post"> <input name="title" /> <button disabled={submission.pending}> {submission.pending ? "Adding..." : "Add Post"} </button> </form> );}// src/routes/index.jsximport { action, useSubmission } from "@solidjs/router";
const addPost = action(async (formData) => { const title = formData.get("title"); await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { const submission = useSubmission(addPost); return ( <form action={addPost} method="post"> <input name="title" /> <button disabled={submission.pending}> {submission.pending ? "Adding..." : "Add Post"} </button> </form> );}Handling errors
To handle errors that occur within an action:
- Import
useSubmissionfrom@solidjs/router. - Call
useSubmissionwith your action, and use the returnederrorproperty to handle the error.
// src/routes/index.tsximport { Show } from "solid-js";import { action, useSubmission } from "@solidjs/router";
const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { const submission = useSubmission(addPost); return ( <form action={addPost} method="post"> <Show when={submission.error}> <p>{submission.error.message}</p> </Show> <input name="title" /> <button>Add Post</button> </form> );}// src/routes/index.jsximport { Show } from "solid-js";import { action, useSubmission } from "@solidjs/router";
const addPost = action(async (formData) => { const title = formData.get("title"); await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { const submission = useSubmission(addPost); return ( <form action={addPost} method="post"> <Show when={submission.error}> <p>{submission.error.message}</p> </Show> <input name="title" /> <button>Add Post</button> </form> );}Validating form fields
To validate form fields in an action:
- Add validation logic in your action and return validation errors if the data is invalid.
- Import
useSubmissionfrom@solidjs/router. - Call
useSubmissionwith your action, and use the returnedresultproperty to handle the errors.
// src/routes/index.tsximport { Show } from "solid-js";import { action, useSubmission } from "@solidjs/router";
const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; if (!title || title.length < 2) { return { error: "Title must be at least 2 characters", }; } await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { const submission = useSubmission(addPost); return ( <form action={addPost} method="post"> <input name="title" /> <Show when={submission.result?.error}> <p>{submission.result?.error}</p> </Show> <button>Add Post</button> </form> );}// src/routes/index.jsximport { Show } from "solid-js";import { action, useSubmission } from "@solidjs/router";
const addPost = action(async (formData) => { const title = formData.get("title"); if (!title || title.length < 2) { return { error: "Title must be at least 2 characters", }; } await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { const submission = useSubmission(addPost); return ( <form action={addPost} method="post"> <input name="title" /> <Show when={submission.result?.error}> <p>{submission.result?.error}</p> </Show> <button>Add Post</button> </form> );}Showing optimistic UI
To update the UI before the server responds:
- Import
useSubmissionfrom@solidjs/router. - Call
useSubmissionwith your action, and use the returnedpendingandinputproperties to display optimistic UI.
// src/routes/index.tsximport { For, Show } from "solid-js";import { action, useSubmission, query, createAsync } from "@solidjs/router";
const getPosts = query(async () => { const posts = await fetch("https://my-api.com/blog"); return await posts.json();}, "posts");
const addPost = action(async (formData: FormData) => { const title = formData.get("title"); await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { const posts = createAsync(() => getPosts()); const submission = useSubmission(addPost); return ( <main> <form action={addPost} method="post"> <input name="title" /> <button>Add Post</button> </form> <ul> <For each={posts()}>{(post) => <li>{post.title}</li>}</For> <Show when={submission.pending}> {submission.input?.[0]?.get("title")?.toString()} </Show> </ul> </main> );}// src/routes/index.jsximport { For, Show } from "solid-js";import { action, useSubmission, query, createAsync } from "@solidjs/router";
const getPosts = query(async () => { const posts = await fetch("https://my-api.com/blog"); return await posts.json();}, "posts");
const addPost = action(async (formData) => { const title = formData.get("title"); await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { const posts = createAsync(() => getPosts()); const submission = useSubmission(addPost); return ( <main> <form action={addPost} method="post"> <input name="title" /> <button>Add Post</button> </form> <ul> <For each={posts()}>{(post) => <li>{post.title}</li>}</For> <Show when={submission.pending}> {submission.input?.[0]?.get("title")?.toString()} </Show> </ul> </main> );}Multiple Submissions
If you want to display optimistic UI for multiple concurrent submissions, you can use the useSubmissions primitive.
Redirecting
To redirect users to a different route within an action:
- Import
redirectfrom@solidjs/router. - Call
redirectwith the route you want to navigate to, and throw its response.
// src/routes/index.tsximport { action, redirect } from "@solidjs/router";
const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; const response = await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), }); const post = await response.json(); throw redirect(`/posts/${post.id}`);}, "addPost");
export default function Page() { return ( <form action={addPost} method="post"> <input name="title" /> <button>Add Post</button> </form> );}// src/routes/index.jsximport { action, redirect } from "@solidjs/router";
const addPost = action(async (formData) => { const title = formData.get("title"); const response = await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), }); const post = await response.json(); throw redirect(`/posts/${post.id}`);}, "addPost");
export default function Page() { return ( <form action={addPost} method="post"> <input name="title" /> <button>Add Post</button> </form> );}Using a database or an ORM
To safely interact with your database or ORM in an action, ensure it's server-only by adding "use server" as the first line of your action:
// src/routes/index.tsximport { action } from "@solidjs/router";import { db } from "~/lib/db";
const addPost = action(async (formData: FormData) => { "use server"; const title = formData.get("title") as string; await db.insert("posts").values({ title });}, "addPost");
export default function Page() { return ( <form action={addPost} method="post"> <input name="title" /> <button>Add Post</button> </form> );}// src/routes/index.jsximport { action } from "@solidjs/router";import { db } from "~/lib/db";
const addPost = action(async (formData) => { "use server"; const title = formData.get("title"); await db.insert("posts").values({ title });}, "addPost");
export default function Page() { return ( <form action={addPost} method="post"> <input name="title" /> <button>Add Post</button> </form> );}Invoking an action programmatically
To programmatically invoke an action:
- Import
useActionfrom@solidjs/router. - Call
useActionwith your action, and use the returned function to invoke the action.
// src/routes/index.tsximport { createSignal } from "solid-js";import { action, useAction } from "@solidjs/router";
const addPost = action(async (title: string) => { await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { const [title, setTitle] = createSignal(""); const addPostAction = useAction(addPost); return ( <div> <input value={title()} onInput={(e) => setTitle(e.target.value)} /> <button onClick={() => addPostAction(title())}>Add Post</button> </div> );}// src/routes/index.jsximport { createSignal } from "solid-js";import { action, useAction } from "@solidjs/router";
const addPost = action(async (title) => { await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), });}, "addPost");
export default function Page() { const [title, setTitle] = createSignal(""); const addPostAction = useAction(addPost); return ( <div> <input value={title()} onInput={(e) => setTitle(e.target.value)} /> <button onClick={() => addPostAction(title())}>Add Post</button> </div> );}