|
| 1 | +--- |
| 2 | +title: "Actions and Mutations" |
| 3 | +--- |
| 4 | + |
| 5 | +This guide provides practical examples of using actions to mutate data in SolidStart. |
| 6 | + |
| 7 | +## Handling form submission |
| 8 | + |
| 9 | +SolidStart extends the HTML [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) element to allow actions to be invoked with the `action` prop. You can use it to handle form submissions: |
| 10 | + |
| 11 | +1. Create an action with a unique name |
| 12 | +2. Pass your action to the `<form>` element in the `action` prop |
| 13 | +3. Make sure the `<form>` element uses the `post` method |
| 14 | + |
| 15 | +```tsx {3-9} {13} title="src/routes/index.tsx" |
| 16 | +import { action } from "@solidjs/router"; |
| 17 | + |
| 18 | +const addPost = action(async (formData: FormData) => { |
| 19 | + const title = formData.get("title"); |
| 20 | + await fetch("https://my-api.com/posts", { |
| 21 | + method: "POST", |
| 22 | + body: JSON.stringify({ title }), |
| 23 | + }); |
| 24 | +}, "addPost"); |
| 25 | + |
| 26 | +export default function Page() { |
| 27 | + return ( |
| 28 | + <form action={addPost} method="post"> |
| 29 | + <input name="title" /> |
| 30 | + <button>Add Post</button> |
| 31 | + </form> |
| 32 | + ); |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | +When invoked in a form, the action automatically receives the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData) object. You can extract the field data using the native `FormData` methods. |
| 37 | + |
| 38 | +If your action accepts typed data, you can use the `with` method to pass custom data to the action: |
| 39 | + |
| 40 | +```tsx {14} title="src/routes/index.tsx" |
| 41 | +import { createSignal } from "solid-js"; |
| 42 | +import { action } from "@solidjs/router"; |
| 43 | + |
| 44 | +const addPost = action(async (title: string) => { |
| 45 | + await fetch("https://my-api.com/posts", { |
| 46 | + method: "POST", |
| 47 | + body: JSON.stringify({ title }), |
| 48 | + }); |
| 49 | +}, "addPost"); |
| 50 | + |
| 51 | +export default function Page() { |
| 52 | + const [title, setTitle] = createSignal(""); |
| 53 | + return ( |
| 54 | + <form action={addPost.with(title())} method="post"> |
| 55 | + <input value={title()} onChange={(e) => setTitle(e.target.value)} /> |
| 56 | + <button>Add Post</button> |
| 57 | + </form> |
| 58 | + ); |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +Note that the action takes a `string` as its parameter rather than a `FormData`. |
| 63 | + |
| 64 | +## Showing loading UI |
| 65 | + |
| 66 | +To display a loading UI while the action is being executed, you can use the [`useSubmission`](/solid-router/reference/data-apis/use-submission) primitive: |
| 67 | + |
| 68 | +1. Import `useSubmission` from `@solidjs/router` |
| 69 | +2. Call `useSubmission` with your action |
| 70 | +3. Use the `pending` property to show a loading state |
| 71 | + |
| 72 | +```tsx {12} {16-18} title="src/routes/index.tsx" |
| 73 | +import { action, useSubmission } from "@solidjs/router"; |
| 74 | + |
| 75 | +const addPost = action(async (formData: FormData) => { |
| 76 | + const title = formData.get("title"); |
| 77 | + await fetch("https://my-api.com/posts", { |
| 78 | + method: "POST", |
| 79 | + body: JSON.stringify({ title }), |
| 80 | + }); |
| 81 | +}, "addPost"); |
| 82 | + |
| 83 | +export default function Page() { |
| 84 | + const submission = useSubmission(addPost); |
| 85 | + return ( |
| 86 | + <form action={addPost} method="post"> |
| 87 | + <input name="title" /> |
| 88 | + <button disabled={submission.pending}> |
| 89 | + {submission.pending ? "Adding..." : "Add Post"} |
| 90 | + </button> |
| 91 | + </form> |
| 92 | + ); |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +## Handling errors |
| 97 | + |
| 98 | +If an error occurs within an action, the error can be accessed using the [`useSubmission`](/solid-router/reference/data-apis/use-submission) primitive: |
| 99 | + |
| 100 | +1. Import `useSubmission` from `@solidjs/router` |
| 101 | +2. Call `useSubmission` with your action |
| 102 | +3. Use the `error` property to show the error message |
| 103 | + |
| 104 | +```tsx {13} {16-18} title="src/routes/index.tsx" |
| 105 | +import { Show } from "solid-js"; |
| 106 | +import { action, useSubmission } from "@solidjs/router"; |
| 107 | + |
| 108 | +const addPost = action(async (formData: FormData) => { |
| 109 | + const title = formData.get("title"); |
| 110 | + await fetch("https://my-api.com/posts", { |
| 111 | + method: "POST", |
| 112 | + body: JSON.stringify({ title }), |
| 113 | + }); |
| 114 | +}, "addPost"); |
| 115 | + |
| 116 | +export default function Page() { |
| 117 | + const submission = useSubmission(addPost); |
| 118 | + return ( |
| 119 | + <form action={addPost} method="post"> |
| 120 | + <Show when={submission.error}> |
| 121 | + <p>{submission.error.message}</p> |
| 122 | + </Show> |
| 123 | + <input name="title" /> |
| 124 | + <button>Add Post</button> |
| 125 | + </form> |
| 126 | + ); |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +## Validating form fields |
| 131 | + |
| 132 | +To validate form fields before submission, you can use the [`useSubmission`](/solid-router/reference/data-apis/use-submission) primitive: |
| 133 | + |
| 134 | +1. Add validation logic in your action and return validation errors if the data is invalid |
| 135 | +2. Import `useSubmission` from `@solidjs/router` |
| 136 | +3. Call `useSubmission` with your action |
| 137 | +4. Display validation errors using the `result` property |
| 138 | + |
| 139 | +```tsx {6-10} {17} {22-24} title="src/routes/index.tsx" |
| 140 | +import { Show } from "solid-js"; |
| 141 | +import { action, useSubmission } from "@solidjs/router"; |
| 142 | + |
| 143 | +const addPost = action(async (formData: FormData) => { |
| 144 | + const title = formData.get("title") as string; |
| 145 | + if (!title || title.length < 2) { |
| 146 | + return { |
| 147 | + error: "Title must be at least 2 characters", |
| 148 | + }; |
| 149 | + } |
| 150 | + await fetch("https://my-api.com/posts", { |
| 151 | + method: "POST", |
| 152 | + body: JSON.stringify({ title }), |
| 153 | + }); |
| 154 | +}, "addPost"); |
| 155 | + |
| 156 | +export default function Page() { |
| 157 | + const submission = useSubmission(addPost); |
| 158 | + return ( |
| 159 | + <form action={addPost} method="post"> |
| 160 | + <input name="title" /> |
| 161 | + <Show when={submission.result?.error}> |
| 162 | + <p>{submission.result?.error}</p> |
| 163 | + </Show> |
| 164 | + <button>Add Post</button> |
| 165 | + </form> |
| 166 | + ); |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +## Using a database or an ORM |
| 171 | + |
| 172 | +To safely interact with your database or ORM in an action, ensure the action is server-only to prevent exposing your database credentials to the client. You can do this by adding [`"use server"`](/solid-start/reference/server/use-server) as the first line of your action: |
| 173 | + |
| 174 | +```tsx {5} title="src/routes/index.tsx" |
| 175 | +import { action } from "@solidjs/router"; |
| 176 | +import { db } from "~/lib/db"; |
| 177 | + |
| 178 | +const addPost = action(async (formData: FormData) => { |
| 179 | + "use server"; |
| 180 | + const title = formData.get("title"); |
| 181 | + await db.insert("posts").values({ title }); |
| 182 | +}, "addPost"); |
| 183 | + |
| 184 | +export default function Page() { |
| 185 | + return ( |
| 186 | + <form action={addPost} method="post"> |
| 187 | + <input name="title" /> |
| 188 | + <button>Add Post</button> |
| 189 | + </form> |
| 190 | + ); |
| 191 | +} |
| 192 | +``` |
| 193 | + |
| 194 | +## Showing optimistic UI |
| 195 | + |
| 196 | +To update the UI before the server responds, you can use the [`useSubmission`](/solid-router/reference/data-apis/use-submission) primitive: |
| 197 | + |
| 198 | +1. Import `useSubmission` from `@solidjs/router` |
| 199 | +2. Call `useSubmission` with your action |
| 200 | +3. Use the `pending` and `input` properties to display optimistic UI |
| 201 | + |
| 202 | +```tsx {19} {28-30} title="src/routes/index.tsx" |
| 203 | +import { For, Show } from "solid-js"; |
| 204 | +import { action, useSubmission, query, createAsync } from "@solidjs/router"; |
| 205 | + |
| 206 | +const getPosts = query(async () => { |
| 207 | + const posts = await fetch("https://my-api.com/blog"); |
| 208 | + return await posts.json(); |
| 209 | +}, "posts"); |
| 210 | + |
| 211 | +const addPost = action(async (formData: FormData) => { |
| 212 | + const title = formData.get("title"); |
| 213 | + await fetch("https://my-api.com/posts", { |
| 214 | + method: "POST", |
| 215 | + body: JSON.stringify({ title }), |
| 216 | + }); |
| 217 | +}, "addPost"); |
| 218 | + |
| 219 | +export default function Page() { |
| 220 | + const posts = createAsync(() => getPosts()); |
| 221 | + const submission = useSubmission(addPost); |
| 222 | + return ( |
| 223 | + <main> |
| 224 | + <form action={addPost} method="post"> |
| 225 | + <input name="title" /> |
| 226 | + <button>Add Post</button> |
| 227 | + </form> |
| 228 | + <ul> |
| 229 | + <For each={posts()}>{(post) => <li>{post.title}</li>}</For> |
| 230 | + <Show when={submission.pending}> |
| 231 | + {submission.input?.[0]?.get("title")?.toString()} |
| 232 | + </Show> |
| 233 | + </ul> |
| 234 | + </main> |
| 235 | + ); |
| 236 | +} |
| 237 | +``` |
| 238 | + |
| 239 | +<Callout type="info" title="Multipe Submissions"> |
| 240 | + If you want to display optimistic UI for multiple concurrent submissions, you can use the [`useSubmissions`](/solid-router/reference/data-apis/use-submissions) primitive. |
| 241 | +</Callout> |
| 242 | + |
| 243 | +## Calling an action manually |
| 244 | + |
| 245 | +If you don't want to use a `<form>` element, you can use the `useAction` primitive to manually call an action: |
| 246 | + |
| 247 | +1. Import `useAction` from `@solidjs/router` |
| 248 | +2. Call `useAction` with your action |
| 249 | +3. Use the returned function to trigger the action |
| 250 | + |
| 251 | +```tsx {13} {17} title="src/routes/index.tsx" |
| 252 | +import { createSignal } from "solid-js"; |
| 253 | +import { action, useAction } from "@solidjs/router"; |
| 254 | + |
| 255 | +const addPost = action(async (title: string) => { |
| 256 | + await fetch("https://my-api.com/posts", { |
| 257 | + method: "POST", |
| 258 | + body: JSON.stringify({ title }), |
| 259 | + }); |
| 260 | +}, "addPost"); |
| 261 | + |
| 262 | +export default function Page() { |
| 263 | + const [title, setTitle] = createSignal(""); |
| 264 | + const addPostAction = useAction(addPost); |
| 265 | + return ( |
| 266 | + <div> |
| 267 | + <input value={title()} onInput={(e) => setTitle(e.target.value)} /> |
| 268 | + <button onClick={() => addPostAction(title())}>Add Post</button> |
| 269 | + </div> |
| 270 | + ); |
| 271 | +} |
| 272 | +``` |
0 commit comments