2

Goal realization effect: Intercept routing changes for custom processing. For example, when a form has not been filled out, the user will leave the current page. At this time, a reminder needs to be given to the user, as shown in the following figure
image.png

Let me talk about the background knowledge first: React-router is composed of three libraries history、react-router、react-router-dom What we usually need to use is react-router-dom

The v5 version implements routing interception

  • When using the v5 version, route interception was implemented like this

     // 文档:https://v5.reactrouter.com/core/api/Prompt
      <Prompt
        when={boolean} // 组件何时激活
        message={(location, action) => {
          // 做一些拦截操作 location 要前往的路由,此时可以先保存下来后续使用
          // return false 取消跳转 比如此时弹起一个自定义弹窗,
          // return true 允许跳转
        }}
      />

v6 version implementation

  • The v6 version is gone Prompt component, after Google search, I found this stackoverflow v6 beta and provided two hooks useBlocker / usePrompt can be used to intercept routing, However, these two hooks were removed when the official version was released. There was a discussion in this issue , and someone here found a solution to add the two deleted hooks back 😂
  • In fact, the routing interception function mainly uses the history block method in the library, here is the relevant code
  • histoy block documentation

    history.block will call your callback for all in-page navigation attempts, but for navigation that reloads the page (eg the refresh button or a link that doesn't use history.push) it registers a beforeunload handler to prevent the navigation. In modern browsers you are not able to customize this dialog. Instead, you'll see something like this (Chrome):
  • The simple translation is histoy.block will block all navigation in the page and call the callback, but directly closing the tab page or refreshing will register beforeunload event and then trigger the browser's default query pop-up window , does not support removing the default pop-up box, I used a hack method below to remove the default query pop-up box
    image.png
  • full code

     import { History, Transition } from 'history'
    import { useContext, useEffect } from 'react'
    import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom'
    
    type ExtendNavigator = Navigator & Pick<History, 'block'>
    
    export function useBlocker(blocker: (tx: Transition) => void, when = true) {
      const { navigator } = useContext(NavigationContext)
    
      useEffect(() => {
        if (!when) return
        // 如不需要刷新页面或关闭tab时取消浏览器询问弹窗,下面的绑定事件则不需要
        window.addEventListener('beforeunload', removeBeforeUnload)
        const unblock = (navigator as any as ExtendNavigator).block(tx => {
          const autoUnblockingTx = {
            ...tx,
            retry() {
              unblock()
              tx.retry()
            },
          }
          blocker(autoUnblockingTx)
        })
        // 由于无法直接 remove history 库中绑定的 beforeunload 事件,只能自己在绑定一个 beforeunload 事件(在原事件之前),触发时调用 unblock
        // 
        function removeBeforeUnload() {
          unblock()
        }
        return () => {
          unblock()
          window.removeEventListener('beforeunload', removeBeforeUnload)
        }
      }, [when])
    }
  • Use useBlocker

     export default function UnsavedPrompt({ when }: Iprops): JSX.Element {
        const [open, setOpen] = useState(false)
        const blockRef = useRef<any>(null)
        useBlocker(tx => {
          setOpen(true)
          blockRef.current = tx
        }, when)
        return (
          <Modal
            open={open}
            toggle={() => setOpen(false)}
            onCancel={() => blockRef.current?.retry()}
            onOk={() => setOpen(false)}
          >
            <p className='text-center text-light-700 text-sm'>
              You have unsaved change, exit without saving?
            </p>
          </Modal>
        )
      }

Notice

  • The time of writing this article is 2022-08-11, the latest version of ---c667c4c3a6ab53b23b92e316fc33cda4 react-router/react-router-dom is 6.3.0 , and this function may be added later with the upgrade of react-router-dom. Code is for reference only

Dividing line

The writing method of React-router v6 routing interception has been shared above. Next, I will record how to compare the conditions of form changes to trigger routing interception. It mainly implements a hook of useCompare to do it.
  • Analysis: To compare whether the data has changed before and after the form, it is nothing more than to save the initial data of the form, and then compare it deeply with the form being operated. However, since the form has an input component, the frequency of its changes is compared. It's fast, so we need to do anti-shake processing. The following is the specific code of useCompare
  • Let's take a look at how useCompare is used

     import { useCompare } from 'hooks/useDebounce'
    
    const compareFunc = useCompare()
    useEffect(() => {
      compareFunc(formData, formDataInit, (flag: boolean) => setFormIsDirty(flag))
    }, [formData, formDataInit])
    // 这里的 formDataInit 一般要在初始状态时从 formData 深拷贝一份出来
  • useCompare implementation

     type Tcb = (args: boolean) => void
    
    // debounce/compare hooks
    export function useCompare() {
      const compareFunc = useDebounce(compare)
      return compareFunc
    }
    
    function compare(a: any, b: any, fn: Tcb) {
      fn(!isEqual(a, b))
    }
  • Because to do anti-shake processing, we must first implement a useDebounce, here I choose to write it myself, there is no ready-made

     import { useRef } from 'react'
    
    type TdebounceFnType = (...args: any[]) => void
    
    export default function useDebounce(fn: TdebounceFnType, wait = 1000) {
      const debounceFnRef = useRef(debounce(fn, wait))
      return debounceFnRef.current
    }
    
    // debounce 原始函数
    export function debounce(this: any, fn: TdebounceFnType, wait = 1000) {
        let timer: NodeJS.Timeout | null = null
        const ctx = this
        return function (...args: any[]) {
          timer && clearTimeout(timer)
          timer = setTimeout(() => {
            fn.apply(ctx, args)
          }, wait)
        }
      }

大桔子
588 声望51 粉丝

下一步该干什么呢...


引用和评论

0 条评论