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 theaction
prop. - Ensure the
<form>
element uses thepost
method for submission. - Use the
FormData
object in the action to extract field data using the naviteFormData
methods.
// 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
useSubmission
from@solidjs/router
. - Call
useSubmission
with your action, and use the returnedpending
property 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
useSubmission
from@solidjs/router
. - Call
useSubmission
with your action, and use the returnederror
property 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
useSubmission
from@solidjs/router
. - Call
useSubmission
with your action, and use the returnedresult
property 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
useSubmission
from@solidjs/router
. - Call
useSubmission
with your action, and use the returnedpending
andinput
properties 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> );}
info
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
redirect
from@solidjs/router
. - Call
redirect
with 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
useAction
from@solidjs/router
. - Call
useAction
with 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> );}