Actions
One question you will likely have when developing any sort of app is "how do I communicate new information to my server?". The user did something. What next? Solid's answer to this is actions. Actions give you the ability an specify an async action processing function and gives you elegant tools to help you easily manage and track submissions.
They generally represent a POST
request.
Actions are isomorphic. This means that a submission can be handled on the server or the client, which ever is optimal. They represent the server component of a HTML form, and even help you use HTML forms to submit data.
Creating actions
Let's stop getting ahead of ourselves! First let's create an action!
tsx
import { createRouteAction(alias) function createRouteAction<T = void, U = void>(fn: (arg1: void, event: ActionEvent) => Promise<U>, options?: {
invalidate?: string | any[] | ((r: Response) => string | any[] | void) | undefined;
} | undefined): RouteAction<T, U> (+1 overload)
import createRouteAction
} from "solid-start/data";
export function MyComponentfunction MyComponent(): void
() { const [_const _: {
pending: boolean;
input?: string | undefined;
result?: void | undefined;
error?: any;
clear: () => void;
retry: () => void;
}
, logMessageconst logMessage: ((vars: string) => Promise<void>) & {
Form: never;
url: string;
}
] = createRouteAction(alias) createRouteAction<string, void>(fn: (args: string, event: ActionEvent) => Promise<void>, options?: {
invalidate?: string | any[] | ((r: Response) => string | void | any[]) | undefined;
} | undefined): RouteAction<...> (+1 overload)
import createRouteAction
(async (message(parameter) message: string
: string) => { // Imagine this is a call to fetch
await new PromiseCreates a new Promise.
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
((resolve(parameter) resolve: (value: unknown) => void
, reject(parameter) reject: (reason?: any) => void
) => setTimeoutfunction setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
(resolve(parameter) resolve: (value: unknown) => void
, 1000)); console.log(method) Console.log(...data: any[]): void
(message(parameter) message: string
); });
}
tsx
import { createRouteAction(alias) function createRouteAction<T = void, U = void>(fn: (arg1: void, event: ActionEvent) => Promise<U>, options?: {
invalidate?: string | any[] | ((r: Response) => string | any[] | void) | undefined;
} | undefined): RouteAction<T, U> (+1 overload)
import createRouteAction
} from "solid-start/data";
export function MyComponentfunction MyComponent(): void
() { const [_const _: {
pending: boolean;
input?: string | undefined;
result?: void | undefined;
error?: any;
clear: () => void;
retry: () => void;
}
, logMessageconst logMessage: ((vars: string) => Promise<void>) & {
Form: never;
url: string;
}
] = createRouteAction(alias) createRouteAction<string, void>(fn: (args: string, event: ActionEvent) => Promise<void>, options?: {
invalidate?: string | any[] | ((r: Response) => string | void | any[]) | undefined;
} | undefined): RouteAction<...> (+1 overload)
import createRouteAction
(async (message(parameter) message: string
: string) => { // Imagine this is a call to fetch
await new PromiseCreates a new Promise.
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
((resolve(parameter) resolve: (value: unknown) => void
, reject(parameter) reject: (reason?: any) => void
) => setTimeoutfunction setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
(resolve(parameter) resolve: (value: unknown) => void
, 1000)); console.log(method) Console.log(...data: any[]): void
(message(parameter) message: string
); });
}
This echo
action will act as your backend, however you can substitute it for any API, provided you are ok with it running on the client. Typically, route actions are used with some sort of solution like fetch or graphql, and return either a Response
such as a redirect (we are not returning anything quite yet!) or any value. If you want to ensure the action only runs on the server for things like databases, you will want to use createServerAction$
. It's been introduced below.
Naturally, this action won't do anything quite yet. We still need to call it somewhere! For now, let's call it manually from some component using the submit
method.
ts
import { createRouteAction(alias) function createRouteAction<T = void, U = void>(fn: (arg1: void, event: ActionEvent) => Promise<U>, options?: {
invalidate?: string | any[] | ((r: Response) => string | any[] | void) | undefined;
} | undefined): RouteAction<T, U> (+1 overload)
import createRouteAction
} from "solid-start/data"; export function MyComponentfunction MyComponent(): void
() { const [, logMessageconst logMessage: ((vars: string) => Promise<void>) & {
Form: never;
url: string;
}
] = createRouteAction(alias) createRouteAction<string, void>(fn: (args: string, event: ActionEvent) => Promise<void>, options?: {
invalidate?: string | any[] | ((r: Response) => string | void | any[]) | undefined;
} | undefined): RouteAction<...> (+1 overload)
import createRouteAction
(async (message(parameter) message: string
: string) => { // Imagine this is a call to fetch
await new PromiseCreates a new Promise.
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
((resolve(parameter) resolve: (value: unknown) => void
, reject(parameter) reject: (reason?: any) => void
) => setTimeoutfunction setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
(resolve(parameter) resolve: (value: unknown) => void
, 1000)); console.log(method) Console.log(...data: any[]): void
(message(parameter) message: string
); });
logMessageconst logMessage: (vars: string) => Promise<void>
("Hello from solid!"); }
ts
import { createRouteAction(alias) function createRouteAction<T = void, U = void>(fn: (arg1: void, event: ActionEvent) => Promise<U>, options?: {
invalidate?: string | any[] | ((r: Response) => string | any[] | void) | undefined;
} | undefined): RouteAction<T, U> (+1 overload)
import createRouteAction
} from "solid-start/data"; export function MyComponentfunction MyComponent(): void
() { const [, logMessageconst logMessage: ((vars: string) => Promise<void>) & {
Form: never;
url: string;
}
] = createRouteAction(alias) createRouteAction<string, void>(fn: (args: string, event: ActionEvent) => Promise<void>, options?: {
invalidate?: string | any[] | ((r: Response) => string | void | any[]) | undefined;
} | undefined): RouteAction<...> (+1 overload)
import createRouteAction
(async (message(parameter) message: string
: string) => { // Imagine this is a call to fetch
await new PromiseCreates a new Promise.
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
((resolve(parameter) resolve: (value: unknown) => void
, reject(parameter) reject: (reason?: any) => void
) => setTimeoutfunction setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
(resolve(parameter) resolve: (value: unknown) => void
, 1000)); console.log(method) Console.log(...data: any[]): void
(message(parameter) message: string
); });
logMessageconst logMessage: (vars: string) => Promise<void>
("Hello from solid!"); }
You should see Hello from solid!
back in the console!
Returning from actions
In many cases, after submitting data the server sends some data back as well. Anything returned from your action function can be accessed using the reactive action.value
property. The value of this property can change each time you submit your action.
tsx
export function MyComponentfunction MyComponent(): JSX.Element
() { const [echoingconst echoing: {
pending: boolean;
input?: string | undefined;
result?: string | undefined;
error?: any;
clear: () => void;
retry: () => void;
}
, echoconst echo: ((vars: string) => Promise<string>) & {
Form: never;
url: string;
}
] = createRouteAction(alias) createRouteAction<string, string>(fn: (args: string, event: ActionEvent) => Promise<string>, options?: {
invalidate?: string | any[] | ((r: Response) => string | void | any[]) | undefined;
} | undefined): RouteAction<...> (+1 overload)
import createRouteAction
(async (message(parameter) message: string
: string) => { await new PromiseCreates a new Promise.
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
((resolve(parameter) resolve: (value: unknown) => void
, reject(parameter) reject: (reason?: any) => void
) => setTimeoutfunction setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
(resolve(parameter) resolve: (value: unknown) => void
, 1000)); return message(parameter) message: string
; });
echoconst echo: (vars: string) => Promise<string>
("Hello from solid!"); setTimeoutfunction setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
(() => echoconst echo: (vars: string) => Promise<string>
("This is a second submission!"), 1500); return <p(property) JSX.IntrinsicElements.p: JSX.HTMLAttributes<HTMLParagraphElement>
>{echoingconst echoing: {
pending: boolean;
input?: string | undefined;
result?: string | undefined;
error?: any;
clear: () => void;
retry: () => void;
}
.result(property) result?: string | undefined
}</p(property) JSX.IntrinsicElements.p: JSX.HTMLAttributes<HTMLParagraphElement>
>; }
tsx
export function MyComponentfunction MyComponent(): JSX.Element
() { const [echoingconst echoing: {
pending: boolean;
input?: string | undefined;
result?: string | undefined;
error?: any;
clear: () => void;
retry: () => void;
}
, echoconst echo: ((vars: string) => Promise<string>) & {
Form: never;
url: string;
}
] = createRouteAction(alias) createRouteAction<string, string>(fn: (args: string, event: ActionEvent) => Promise<string>, options?: {
invalidate?: string | any[] | ((r: Response) => string | void | any[]) | undefined;
} | undefined): RouteAction<...> (+1 overload)
import createRouteAction
(async (message(parameter) message: string
: string) => { await new PromiseCreates a new Promise.
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
((resolve(parameter) resolve: (value: unknown) => void
, reject(parameter) reject: (reason?: any) => void
) => setTimeoutfunction setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
(resolve(parameter) resolve: (value: unknown) => void
, 1000)); return message(parameter) message: string
; });
echoconst echo: (vars: string) => Promise<string>
("Hello from solid!"); setTimeoutfunction setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
(() => echoconst echo: (vars: string) => Promise<string>
("This is a second submission!"), 1500); return <p(property) JSX.IntrinsicElements.p: JSX.HTMLAttributes<HTMLParagraphElement>
>{echoingconst echoing: {
pending: boolean;
input?: string | undefined;
result?: string | undefined;
error?: any;
clear: () => void;
retry: () => void;
}
.result(property) result?: string | undefined
}</p(property) JSX.IntrinsicElements.p: JSX.HTMLAttributes<HTMLParagraphElement>
>; }
While this method of using actions works, it leaves the implementation details of how you trigger echo
up to you. When handling explicit user input, it's better to use a form
for a multitude of reasons.
We highly recommend using HTML forms as your method to submit data with actions. HTML forms can be used even before JavaScript loads, leading to instantly interactive applications. They have the added benefit of implicit accessibility, and can save you valuable time that would have otherwise been spent designing a UI library that will never have the aforementioned benefits.
When forms are used to submit actions, the first argument is an instance of FormData
. Writing forms using actions is trivial, simply use the Form
method of your action instead of the normal <form>
tag, and walk away with amazing, progressively enhanced forms!
If you don't return a Response
from your action, the user will stay on the same page and your resources will be retriggered. You can also return a redirect
or ResponseError
.
tsx
export function MyComponentfunction MyComponent(): JSX.Element
() { const [_const _: {
pending: boolean;
input?: FormData | undefined;
result?: Response | undefined;
error?: any;
clear: () => void;
retry: () => void;
}
, { Formconst Form: ParentComponent<FormProps>
}] = createRouteAction(alias) createRouteAction<FormData, Response>(fn: (args: FormData, event: ActionEvent) => Promise<Response>, options?: {
invalidate?: string | any[] | ((r: Response) => string | ... 1 more ... | any[]) | undefined;
} | undefined): RouteAction<...> (+1 overload)
import createRouteAction
(async (formData(parameter) formData: FormData
: FormDataProvides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data".
interface FormData
) => { await new PromiseCreates a new Promise.
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
((resolve(parameter) resolve: (value: unknown) => void
, reject(parameter) reject: (reason?: any) => void
) => setTimeoutfunction setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
(resolve(parameter) resolve: (value: unknown) => void
, 1000)); const usernameconst username: FormDataEntryValue | null
= formData(parameter) formData: FormData
.get(method) FormData.get(name: string): FormDataEntryValue | null
("username"); if (usernameconst username: FormDataEntryValue | null
=== "admin") { return redirectA redirect response. Sets the status code and the `Location` header.
Defaults to "302 Found".
(alias) redirect(url: string, init?: number | ResponseInit): Response
import redirect
("/admin"); } else {
throw new Errorvar Error: ErrorConstructor
new (message?: string | undefined) => Error
("Invalid username"); }
return redirectA redirect response. Sets the status code and the `Location` header.
Defaults to "302 Found".
(alias) redirect(url: string, init?: number | ResponseInit): Response
import redirect
("/home"); });
return (
<Formconst Form: ParentComponent<FormProps>
> <label(property) JSX.IntrinsicElements.label: JSX.LabelHTMLAttributes<HTMLLabelElement>
for(property) JSX.LabelHTMLAttributes<HTMLLabelElement>.for?: string | undefined
="username">Username:</label(property) JSX.IntrinsicElements.label: JSX.LabelHTMLAttributes<HTMLLabelElement>
> <input(property) JSX.IntrinsicElements.input: JSX.InputHTMLAttributes<HTMLInputElement>
type(property) JSX.InputHTMLAttributes<HTMLInputElement>.type?: string | undefined
="text" name(property) JSX.InputHTMLAttributes<HTMLInputElement>.name?: string | undefined
="username" /> <input(property) JSX.IntrinsicElements.input: JSX.InputHTMLAttributes<HTMLInputElement>
type(property) JSX.InputHTMLAttributes<HTMLInputElement>.type?: string | undefined
="submit" value(property) JSX.InputHTMLAttributes<HTMLInputElement>.value?: string | number | string[] | undefined
="submit" /> </Formconst Form: ParentComponent<FormProps>
> );
}
tsx
export function MyComponentfunction MyComponent(): JSX.Element
() { const [_const _: {
pending: boolean;
input?: FormData | undefined;
result?: Response | undefined;
error?: any;
clear: () => void;
retry: () => void;
}
, { Formconst Form: ParentComponent<FormProps>
}] = createRouteAction(alias) createRouteAction<FormData, Response>(fn: (args: FormData, event: ActionEvent) => Promise<Response>, options?: {
invalidate?: string | any[] | ((r: Response) => string | ... 1 more ... | any[]) | undefined;
} | undefined): RouteAction<...> (+1 overload)
import createRouteAction
(async (formData(parameter) formData: FormData
: FormDataProvides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data".
interface FormData
) => { await new PromiseCreates a new Promise.
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
((resolve(parameter) resolve: (value: unknown) => void
, reject(parameter) reject: (reason?: any) => void
) => setTimeoutfunction setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
(resolve(parameter) resolve: (value: unknown) => void
, 1000)); const usernameconst username: FormDataEntryValue | null
= formData(parameter) formData: FormData
.get(method) FormData.get(name: string): FormDataEntryValue | null
("username"); if (usernameconst username: FormDataEntryValue | null
=== "admin") { return redirectA redirect response. Sets the status code and the `Location` header.
Defaults to "302 Found".
(alias) redirect(url: string, init?: number | ResponseInit): Response
import redirect
("/admin"); } else {
throw new Errorvar Error: ErrorConstructor
new (message?: string | undefined) => Error
("Invalid username"); }
return redirectA redirect response. Sets the status code and the `Location` header.
Defaults to "302 Found".
(alias) redirect(url: string, init?: number | ResponseInit): Response
import redirect
("/home"); });
return (
<Formconst Form: ParentComponent<FormProps>
> <label(property) JSX.IntrinsicElements.label: JSX.LabelHTMLAttributes<HTMLLabelElement>
for(property) JSX.LabelHTMLAttributes<HTMLLabelElement>.for?: string | undefined
="username">Username:</label(property) JSX.IntrinsicElements.label: JSX.LabelHTMLAttributes<HTMLLabelElement>
> <input(property) JSX.IntrinsicElements.input: JSX.InputHTMLAttributes<HTMLInputElement>
type(property) JSX.InputHTMLAttributes<HTMLInputElement>.type?: string | undefined
="text" name(property) JSX.InputHTMLAttributes<HTMLInputElement>.name?: string | undefined
="username" /> <input(property) JSX.IntrinsicElements.input: JSX.InputHTMLAttributes<HTMLInputElement>
type(property) JSX.InputHTMLAttributes<HTMLInputElement>.type?: string | undefined
="submit" value(property) JSX.InputHTMLAttributes<HTMLInputElement>.value?: string | number | string[] | undefined
="submit" /> </Formconst Form: ParentComponent<FormProps>
> );
}
This Form
is an enhanced version of the normal form
. It submit handler has already been wired up as well.
Retriggering resources
- retriggers route resources
Errors
- Errors, error field that's populated if the submission errored, and a status field that's set to
error
- if you read the
submissionState.error
field in your code (JSX, or effects) then the error is considered user-handled and we don't trigger ErrorBoundaries.
- if you don't use the error field, then we trigger the error boundary on an error because we assume its unexpected for you
- How to do form errors? Where to put... Here or
ResponseError
?
Server Actions
Sometimes we need to make sure our action only runs on the server. This is useful for:
- accessing internal APIs
- proxing external APIs
- To use server secrets
- To reduce the response payload by postprocessing
- To bypass CORS
- running code incompatible with browsers
- or even connecting directly to a database (Take caution, opinions on if this is a good idea are mixed. You should consider separating your backend and frontend.)
To do this, simply replace createRouteAction
with createServerAction$
and the action will always be run on the server.