Standardized hook for handling route submission in a Remix app
- Published on
- • 4 mins read
I have been working with Remix for a while now and I love it. Here is a custom hook I use to handle in-route-submission in my Remix apps.
use-route-submission.ts
import { useActionData, useNavigation, useSubmit } from '@remix-run/react'
import { useEffect } from 'react'
import { parseSubmissionData } from '~/utils/object'
import { useIndexRouteDetector } from './use-index-route-detector'
import type { RouteSubmission, RouteSubmissionInput, SubmitData } from '~/types/hooks'
/**
* Submit data to the current route from any nested element.
* Prefer this over `useSubmitFetcher` as it allows the `actionData` from `useActionData` hook can be accessed from any nested component in the route no matter how deep it is.
*
* @param input - `object` - The input to use for the route submission
* @param [input._action] - The `_action` to use for the route submission, it will be added to submit data
* @param [input.onSubmitted] - A callback to run when the route submission is submitted
* @returns RouteSubmission
* @example
* ```tsx
* import { useRouteSubmission } from '~/hooks'
*
* export function MyComponent() {
* // An `_action` param should be added to differentiate between multiple submissions
* let [submit, submitting, submitData] = useRouteSubmission({ _action: 'myAction' })
* // or let {submit, submitting, submitData} = useRouteSubmission({ _action: 'myAction' })
*
* let handleClick = () => submit({ name: 'John Doe' })
* let loading = submitting
*
* return (
* <Button loading={loading} onClick={handleClick}>
* Submit
* </Button>
* )
* }
*```
*/
export function useRouteSubmission(input: RouteSubmissionInput): RouteSubmission {
let _submit = useSubmit()
let navigation = useNavigation()
let isIndexRoute = useIndexRouteDetector()
let actionData = useActionData()
let { _action, onSubmitted } = input || {}
let submitData = parseSubmissionData(navigation)
let isActionMatched = submitData?._action === _action
useEffect(() => {
if (isActionMatched && navigation.state === 'loading') {
onSubmitted?.(submitData, actionData)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation.state])
let submit = (data: SubmitData = {}) => {
let actionURL = window.location.pathname
_submit(
{ data: JSON.stringify({ ...data, _action }) },
{
action: isIndexRoute ? `${actionURL}?index` : actionURL,
method: 'post',
replace: true,
}
)
}
let submitting = isActionMatched && navigation.state === 'submitting'
return Object.defineProperty({ submit, submitting, submitData }, Symbol.iterator, {
enumerable: false,
value: function* () {
yield submit
yield submitting
yield submitData
},
}) as RouteSubmission
}
The utils used in the hook are:
object.ts
import type { useNavigation } from '@remix-run/react'
import type {SubmitDataWithAction} from '~/types/hooks'
/**
* Get the submitted data of a submission
*
* @param navigation - The current page navigation returned from `useNavigation` hook
* @example
* let data = parseSubmissionData(transition)
*/
export function parseSubmissionData(
navigation: ReturnType<typeof useNavigation>,
): SubmitDataWithAction {
let formData = navigation?.formData
if (!formData) return null
return JSON.parse((Object.fromEntries(formData) as any).data)
}
And
use-index-route-detector.ts
import { useLocation, useMatches } from '@remix-run/react'
export function useIndexRouteDetector() {
let matches = useMatches()
let location = useLocation()
let match = matches.find(({ pathname }) => pathname === location.pathname)
if (match) {
return !!match.id.match(/\/index$/) || match.id === 'root'
}
return false
}
And the types used in the hook are:
hooks.ts
export type RouteSubmissionInput = {
_action: string
onSubmitted?: (
submitData: SubmitDataWithAction,
actionData: { [key: string]: any },
) => void
}
export type SubmitData = {
[key: string]: any
}
export type SubmitDataWithAction = {
_action?: string
[key: string]: any
}
type SubmitFunction = (data?: SubmitData) => void
export type RouteSubmission = {
submit: SubmitFunction
submitting: boolean
submitData: SubmitDataWithAction
} & [SubmitFunction, boolean, SubmitDataWithAction]
Example usage:
example.tsx
import { Button } from '~/components/button'
import { useRouteSubmission } from '~/hooks/use-route-submission'
export function SaveProject() {
// An `_action` param should be added to differentiate between multiple submissions
let [submit, submitting] = useRouteSubmission({ _action: 'SAVE_DATA' })
// or let {submit, submitting} = useRouteSubmission({ _action: 'SAVE_DATA' })
function save() {
submit({ name: 'John Doe' })
}
return (
<Button loading={submitting} onClick={save}>
Save
</Button>
)
}
Happy submitting!