在Remix应用中处理路由提交的简单方法

Published on
Published on
/5 mins read/---

我已经使用Remix有一段时间了,我很喜欢它。 这是我在Remix应用中用来处理路由内提交的自定义Hook。

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'
 
/**
 * 从任何嵌套元素向当前路由提交数据。
 * 优先使用此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中使用的工具函数:

object.ts
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)
}

以及

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
}

Hook中使用的类型定义:

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.tsx
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 的优势

  1. 数据访问性: 可以在路由的任何嵌套组件中访问 actionData
  2. 状态统一: 所有组件共享相同的提交状态
  3. 简化逻辑: 不需要手动管理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!