Actions

Actions are used to enable data mutations and will handle all POST navigations. The idiomatic way to submit POST data 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>
}

Submitting form data is also a navigation. The example form will try to navigate to /todos while submitting the data for creating a todo. To handle this POST 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 action 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 useAction 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 = useAction()

	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={action}>
		<Tags path="tags" />
		<CreateTag path="tags/create" />

		<Todos path="todos" />
		<CreateTodo path="todos/create" />
	</App>
)

async function action({ data }) {
	let intent = data.get('intent')
	if (intent === 'create tag') {
		...
	} else if (intent === 'create todo') {
		...
	}
}

function App() {
	let action = useAction()
		...
}

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={applicationAction}>
		<Todos path="todos" />
		<CreateTodo path="todos/create" />

		<Admin action={adminAction}>
			<Tags path="tags" />
			<CreateTag path="tags/create" />
		</Admin>
	</App>
)

async function applicationAction({ 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 = useAction()
		...
}

function Admin() {
	let action = useAction()
		...
}

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.