diff --git a/data/en/snippets/remixing-remix-submission.mdx b/data/en/snippets/remixing-remix-submission.mdx new file mode 100644 index 00000000..2d558ea6 --- /dev/null +++ b/data/en/snippets/remixing-remix-submission.mdx @@ -0,0 +1,182 @@ +--- +heading: 'Remixing remix submission' +title: 'Standardized hook for handling route submission in a Remix app' +date: '2023-09-04' +type: 'Remix' +draft: false +summary: 'A custom hook to handle in-route-submission in a Remix app' +tags: [tag1, tag2, tag3, tag4] +--- + +import Twemoji from './Twemoji.tsx' + +I have been working with [Remix](https://remix.run) for a while now and I love it. +Here is a custom hook I use to handle in-route-submission in my Remix apps. + +````ts: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 ( + * + * ) + * } + *``` + */ +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: + +```ts: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, +): SubmitDataWithAction { + let formData = navigation?.formData + if (!formData) return null + return JSON.parse((Object.fromEntries(formData) as any).data) +} +``` + +And + +```ts: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: + +```ts: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: + +```tsx: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 ( + + ) +} +``` + +Happy submitting!