Remixing remix submission
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.
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:
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
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:
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:
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!
react to this snippet