我已经使用Remix有一段时间了,我很喜欢它。 这是我在Remix应用中用来处理路由内提交的自定义Hook。
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'
/**
* 从任何嵌套元素向当前路由提交数据。
* 优先使用此Hook而不是 `useSubmitFetcher`,因为它允许从路由中任何嵌套组件(无论嵌套多深)
* 访问 `useActionData` Hook 返回的 `actionData`。
*
* @param input - `object` - 路由提交使用的输入参数
* @param [input._action] - 路由提交使用的 `_action`,它会被添加到提交数据中
* @param [input.onSubmitted] - 路由提交完成时运行的回调函数
* @returns RouteSubmission
* @example
* ```tsx
* import { useRouteSubmission } from '~/hooks'
*
* export function MyComponent() {
* // 应该添加 `_action` 参数来区分多个提交
* let [submit, submitting, submitData] = useRouteSubmission({ _action: 'myAction' })
* // 或者 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
}
Hook中使用的工具函数:
import type { useNavigation } from '@remix-run/react'
import type {SubmitDataWithAction} from '~/types/hooks'
/**
* 获取提交的数据
*
* @param navigation - 从 `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)
}
以及
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
}
Hook中使用的类型定义:
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]
使用示例:
import { Button } from '~/components/button'
import { useRouteSubmission } from '~/hooks/use-route-submission'
export function SaveProject() {
// 应该添加 `_action` 参数来区分多个提交
let [submit, submitting] = useRouteSubmission({ _action: 'SAVE_DATA' })
// 或者 let {submit, submitting} = useRouteSubmission({ _action: 'SAVE_DATA' })
function save() {
submit({ name: 'John Doe' })
}
return (
<Button loading={submitting} onClick={save}>
保存
</Button>
)
}
Hook的优势
useSubmitFetcher
的优势
相比 - 数据访问性: 可以在路由的任何嵌套组件中访问
actionData
- 状态统一: 所有组件共享相同的提交状态
- 简化逻辑: 不需要手动管理fetcher状态
主要特性
- 多个提交支持: 通过
_action
参数区分不同的提交操作 - 回调支持: 提交完成后的
onSubmitted
回调 - 数组解构: 支持数组和对象两种解构方式
- 索引路由处理: 自动检测并处理索引路由
实际应用场景
表单保存
function DocumentEditor() {
let [saveDocument, saving] = useRouteSubmission({
_action: 'SAVE_DOCUMENT',
onSubmitted: (data, actionData) => {
if (actionData.success) {
toast.success('文档保存成功')
}
},
})
return (
<form>
{/* 表单字段 */}
<button onClick={() => saveDocument({ title, content })}>
{saving ? '保存中...' : '保存文档'}
</button>
</form>
)
}
删除操作
function DeleteButton({ itemId }: { itemId: string }) {
let [deleteItem, deleting] = useRouteSubmission({ _action: 'DELETE_ITEM' })
return (
<button onClick={() => deleteItem({ itemId })} disabled={deleting}>
{deleting ? '删除中...' : '删除'}
</button>
)
}
Happy submitting!