Skip to content

Commit

Permalink
feat(snippets): remixing remix's submission
Browse files Browse the repository at this point in the history
  • Loading branch information
hta218 committed Sep 4, 2023
1 parent 3054fbc commit dd142b2
Showing 1 changed file with 182 additions and 0 deletions.
182 changes: 182 additions & 0 deletions data/en/snippets/remixing-remix-submission.mdx
Original file line number Diff line number Diff line change
@@ -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 (
* <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:

```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<typeof useNavigation>,
): 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 (
<Button loading={submitting} onClick={save}>
Save
</Button>
)
}
```

Happy submitting! <Twemoji emoji="clinking-beer-mugs" />

1 comment on commit dd142b2

@vercel
Copy link

@vercel vercel bot commented on dd142b2 Sep 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

leo-huynh-dot-dev – ./

leo-huynh-dot-dev-git-main-hta218.vercel.app
www.leohuynh.dev
leo-huynh-dot-dev-hta218.vercel.app
leohuynh.dev

Please sign in to comment.