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}
			...
		</>
	);
}

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>
}