Actions
Actions are used to enable data mutations and will handle all POST navigations. The idiomatic way to submit data with a POST navigation is using a Form element.
import Routes, { Form } from 'react-sprout'
let Router = Routes(
<App>
<Todos path="todos" />
<CreateTodo path="todos/create" />
</App>
)
function Todos() {
...
}
function CreateTodo() {
return <Form action="/todos" method="post">
<input type="text" name="title" />
<button>Create todo</button>
</Form>
}
On the web, submitting form data is also a navigation. The action
property of the form element should be the target url to navigate to.
To handle the POST navigation data, add an action property to the target route in the routes configuration.
This example will navigate to /todos
when submitting the form, so we add an action handler to the Todos
route.
let Router = Routes(
<App>
<Todos path="todos" action={todosAction} />
<CreateTodo path="todos/create" />
</App>
)
async function todosAction({ data }) {
let todo = Object.fromEntries(data)
...
}
The data from the form will be passed as an argument to the action function. It will be a URLSearchParams or FormData instance, depending on the encoding type property of the form.
When the action is completed successfully, the router will start navigating to the target url.
Most likely there will be more than one different type of action that will navigate to a route.
Deleting a todo for example should maybe also show the /todos
overview after the todo is deleted.
To differentiate between different actions you should add a seperate form field to specify the action's intent.
Either add a hidden input field, or attach a name
and value
to the submit button.
let Router = Routes(
<App>
<Todos path="todos" action={todosAction} />
<CreateTodo path="todos/create" />
<DeleteTodo path="todos/delete" />
</App>
)
function CreateTodo() {
return <Form action="/todos" method="post">
<input type="text" name="title" />
<button name="intent" value="create todo">Create todo</button>
</Form>
}
function DeleteTodo() {
return <Form action="/todos" method="post">
<input type="hidden" name="id" value={todo.id} />
<button name="intent" value="delete todo">Delete todo</button>
</Form>
}
async function todosAction({ data }) {
let intent = data.get('intent')
if (intent === 'create todo') {
...
} else if (intent === 'delete todo') {
...
}
}
The route component with the action property can use the result of the action with the useActionResult hook.
async function todosAction({ data }) {
let intent = data.get('intent')
if (intent === 'create todo') {
...
return { message: 'Todo created' }
} else if (intent === 'delete todo') {
...
return { message: 'Todo deleted' }
}
}
function Todos() {
let action = useActionResult()
let message = action?.message
let messageElement
if (message) {
messageElement = <Alert>{message}</Alert>
}
return (
<>
{messageElement}
...
</>
);
}
Remember that a route can always be navigated to without an action. Make sure
the route component using the action result can also handle an undefined
action result.
An action handler can handle the actions of multiple target routes if it is defined higher up in the routes configuration.
let Router = Routes(
<App action={rootAction}>
<Tags path="tags" />
<CreateTag path="tags/create" />
<Todos path="todos" />
<CreateTodo path="todos/create" />
</App>
)
async function rootAction({ data }) {
let intent = data.get('intent')
if (intent === 'create tag') {
...
} else if (intent === 'create todo') {
...
}
}
function App() {
let action = useActionResult()
...
}
This way the App
route component will handle actions for all its child routes. Having one route to handle all actions will make for a uniform way to render all the action results.
A route can always override the way action results are rendered by defining their own action handler.
let Router = Routes(
<App action={rootAction}>
<Todos path="todos" />
<CreateTodo path="todos/create" />
<Admin action={adminAction}>
<Tags path="tags" />
<CreateTag path="tags/create" />
</Admin>
</App>
)
async function rootAction({ data }) {
let intent = data.get('intent')
if (intent === 'create todo') {
...
}
}
async function adminAction({ data }) {
let intent = data.get('intent')
if (intent === 'create tag') {
...
}
}
function App() {
let action = useActionResult()
...
}
function Admin() {
let action = useActionResult()
...
}
All POST navigations to /tags
will be handled in the adminAction
handler. For these navigations only the Admin
route component will have an action result.
Errors
When the action handler throws an error, navigation will not occur. Errors should be handled with the onActionError
callback.
function CreateTodo() {
function handleActionError(event, error) {
...
}
return <Form action="/todos" method="post" onActionError={handleActionError}>
<input type="text" name="title" />
<button>Create todo</button>
</Form>
}