我的个人博客

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

Published on
Published on
/4 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'
 
/**
 * 从任何嵌套元素向当前路由提交数据。
 * 相比`useSubmitFetcher`更推荐使用这个,因为它允许从`useActionData`钩子获取的`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`钩子返回的当前页面导航
 * @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>
  )
}

愉快地提交吧!