使用高阶组件封装路由拦截逻辑

前言

用户在编辑表单时,经常编辑一半中途切换页面,我们需要进行路由拦截,对用户进行询问,是否需要保存当前信息。

React中实现路由拦截使用useBlocker就可以了。本文将使用高阶组件对路由拦截进行封装来扩展功能。

  1. 最基本的路由拦截,默认拦截弹窗
  2. 自定义控制是否需要拦截
  3. 自定义弹窗取消和确定事件
  4. 自定义弹窗样式

后面有代码仓库地址

正文

基本的路由拦截

编写一个WithBlocker来控制路由拦截,在需要拦截的页面使用就行

tsx 复制代码
import { Modal } from 'antd';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useBlocker } from 'react-router-dom';

const WithBlocker = (WrappedComponent: React.ComponentType<any>) => {
  return (props: any) => {
    /** 是否出现弹窗*/
    const [isOpen, setIsOpen] = useState(false);
    const blocker: any = useBlocker(
      ({ currentLocation, nextLocation }) => currentLocation.pathname !== nextLocation.pathname
    );

    useEffect(() => {
      if (blocker.state === 'blocked') {
        setIsOpen(true);
      } else {
        setIsOpen(false);
      }
    }, [blocker]);

    return (
      <>
        <WrappedComponent {...props} />

        <Modal
          title="默认弹窗"
          open={isOpen}
          onCancel={() => {
            setIsOpen(false);
            blocker.reset();
          }}
          onOk={() => {
            setIsOpen(false);
            blocker.proceed();
          }}
        >
          <div>是否退出</div>
        </Modal>
      </>
    );
  };
};

export default WithBlocker;

这就是最基本,只要离开页面就拦截。

tsx 复制代码
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import WithBlocker from '../../feature/RouteInterception/WithBlocker';

const Index = () => {
  return <div>home</div>;
};

export default WithBlocker(Index);

自定义控制是否需要拦截

我们只要控制useBlocker里面函数的返回值就可以控制是否拦截,true拦截,false不拦截。

typescript 复制代码
const blocker: any = useBlocker( ({ currentLocation, nextLocation }) => currentLocation.pathname !== nextLocation.pathname );

使用isIntercept开控制,是否要进行路由拦截

tsx 复制代码
import { Modal } from 'antd';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useBlocker } from 'react-router-dom';

const WithBlocker = (WrappedComponent: React.ComponentType<any>) => {
  return (props: any) => {
    /** 是否出现弹窗*/
    const [isOpen, setIsOpen] = useState(false);
    /** 是否拦截路由*/
    const [isIntercept, setIsIntercept] = useState(true);
    const blocker: any = useBlocker(
      ({ currentLocation, nextLocation }) => isIntercept && currentLocation.pathname !== nextLocation.pathname
    );

    useEffect(() => {
      if (blocker.state === 'blocked') {
        setIsOpen(true);
      } else {
        setIsOpen(false);
      }
    }, [blocker]);

    return (
      <>
        <WrappedComponent {...props} setIsIntercept={setIsIntercept} />

        <Modal
          title="默认弹窗"
          open={isOpen}
          onCancel={() => {
            setIsOpen(false);
            blocker.reset();
          }}
          onOk={() => {
            setIsOpen(false);
            blocker.proceed();
          }}
        >
          <div>是否退出</div>
        </Modal>
      </>
    );
  };
};

export default WithBlocker;

使用场景:修改一个表单,没有发生改变可以直接退出,有修改进行询问。

tsx 复制代码
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import WithBlocker from '../../feature/RouteInterception/WithBlocker';
import { Modal } from 'antd';

const Index = (data: any) => {
  const {  setIsIntercept } = data;

  useEffect(() => {
    setIsIntercept(false);
  }, []);

  return <div>home</div>;
};

export default WithBlocker(Index);

自定义弹窗取消和确定事件

需要自定义事件,我们使用的是高阶组件,事件需要从子元素传递给父元素,那就要使用useImperativeHandle这个hook

tsx 复制代码
import { Modal } from 'antd';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useBlocker } from 'react-router-dom';

const WithBlocker = (WrappedComponent: React.ComponentType<any>) => {
  return (props: any) => {
    const childRef = useRef<{
      proceed: () => Promise<void>;
      reset: () => Promise<void>;
    }>();
    /** 是否出现弹窗*/
    const [isOpen, setIsOpen] = useState(false);
    /** 是否拦截路由*/
    const [isIntercept, setIsIntercept] = useState(true);
    const blocker: any = useBlocker(
      ({ currentLocation, nextLocation }) => isIntercept && currentLocation.pathname !== nextLocation.pathname
    );

    useEffect(() => {
      if (blocker.state === 'blocked') {
        setIsOpen(true);
      } else {
        setIsOpen(false);
      }
    }, [blocker]);

    return (
      <>
        <WrappedComponent ref={childRef} {...props} isOpen={isOpen} setIsIntercept={setIsIntercept} />

        {isIntercept && (
          <Modal
            title="默认弹窗"
            open={isOpen}
            onCancel={() => {
              if (childRef.current) {
                childRef.current
                  .reset()
                  .then(() => {
                    setIsOpen(false);
                    blocker.reset();
                  })
                  .catch(() => {
                    setIsOpen(false);
                    blocker.reset();
                  });
                return;
              }
              setIsOpen(false);
              blocker.reset();
            }}
            onOk={() => {
              if (childRef.current) {
                childRef.current
                  .proceed()
                  .then(() => {
                    setIsOpen(false);
                    blocker.proceed();
                  })
                  .catch(() => {
                    setIsOpen(false);
                    blocker.reset();
                  });
                return;
              }
              setIsOpen(false);
              blocker.proceed();
            }}
          >
            <div>是否退出</div>
          </Modal>
        )}
      </>
    );
  };
};

export default WithBlocker;

使用场景,往往修改完表格退出点击确认,需要调用保存接口,我们使用Promise就可以在请求成功或者失败的时候进行放行和拦截。

tsx 复制代码
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import WithBlocker from '../../feature/RouteInterception/WithBlocker';
import { Modal } from 'antd';

const Index = forwardRef((data: any, ref: any) => {
  const {  setIsIntercept } = data;

  useImperativeHandle(ref, () => ({
    proceed: () => {
      return new Promise((resolve, reject) => {
        //dosomething
        resolve(true);
      });
    },
    reset: () => {
      return new Promise((resolve, reject) => {
        //dosomething
        resolve(true);
      });
    },
  }));

  return <div>home</div>;
});

export default WithBlocker(Index);

自定义弹窗样式

使用isCustomModule来标记是否需要自定义弹窗,blockerComponent来设置自定义的弹窗组件。

tsx 复制代码
import { Modal } from 'antd';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useBlocker } from 'react-router-dom';

const WithBlocker = (WrappedComponent: React.ComponentType<any>) => {
  return (props: any) => {
    const childRef = useRef<{
      proceed: () => Promise<void>;
      reset: () => Promise<void>;
    }>();
    /** 是否出现弹窗*/
    const [isOpen, setIsOpen] = useState(false);
    /** 是否拦截路由*/
    const [isIntercept, setIsIntercept] = useState(true);
    /** 是否使用自定义弹窗*/
    const [isCustomModule, setIsCustomModule] = useState(false);
    const blocker: any = useBlocker(
      ({ currentLocation, nextLocation }) => isIntercept && currentLocation.pathname !== nextLocation.pathname
    );
    /** 自定义弹窗的组件*/
    const [blockerComponent, setBlockerComponent] = useState<any>();
    /** 设置自定义弹窗*/
    const setInterceptionModule = useCallback(
      (Component: React.ComponentType<any>) => {
        if (!isCustomModule) {
          return;
        }

        setBlockerComponent(() => {
          return <Component />;
        });
      },
      [isCustomModule]
    );

    useEffect(() => {
      if (blocker.state === 'blocked') {
        setIsOpen(true);
      } else {
        setIsOpen(false);
      }
    }, [blocker]);

    return (
      <>
        <WrappedComponent
          ref={childRef}
          {...props}
          setIsCustomModule={setIsCustomModule}
          blocker={blocker}
          isOpen={isOpen}
          setInterceptionModule={setInterceptionModule}
          setIsBlocking={setIsOpen}
          setIsIntercept={setIsIntercept}
        />
        {isCustomModule && isIntercept && blockerComponent}
        {!isCustomModule && isIntercept && (
          <Modal
            title="默认弹窗"
            open={isOpen}
            onCancel={() => {
              if (childRef.current) {
                childRef.current
                  .reset()
                  .then(() => {
                    setIsOpen(false);
                    blocker.reset();
                  })
                  .catch(() => {
                    setIsOpen(false);
                    blocker.reset();
                  });
                return;
              }
              setIsOpen(false);
              blocker.reset();
            }}
            onOk={() => {
              if (childRef.current) {
                childRef.current
                  .proceed()
                  .then(() => {
                    setIsOpen(false);
                    blocker.proceed();
                  })
                  .catch(() => {
                    setIsOpen(false);
                    blocker.reset();
                  });
                return;
              }
              setIsOpen(false);
              blocker.proceed();
            }}
          >
            <div>是否退出</div>
          </Modal>
        )}
      </>
    );
  };
};

export default WithBlocker;

使用

tsx 复制代码
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import WithBlocker from '../../feature/RouteInterception/WithBlocker';
import { Modal } from 'antd';

const Index = forwardRef((data: any, ref: any) => {
  const { blocker, isOpen, setInterceptionModule, setIsCustomModule, setIsOpen } = data;

  useEffect(() => {
    setIsCustomModule(true);
    setInterceptionModule(() => {
      return (
        <Modal
          title="自定义弹窗"
          open={isOpen}
          onCancel={() => {
            setIsOpen(false);
            blocker.reset();
          }}
          onOk={() => {
            setIsOpen(false);
            blocker.proceed();
          }}
        >
          <div>sadasd</div>
          <div>sadasd</div>
        </Modal>
      );
    });
  }, [isOpen, blocker]);

  return <div>home</div>;
});

export default WithBlocker(Index);

结语

感兴趣的可以去试试,有更好的写法可以在评论区说说

仓库地址:权限管理和路由拦截 (gitee.com)

相关推荐
小白小白从不日白4 分钟前
react hooks--useCallback
前端·react.js·前端框架
恩婧12 分钟前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
mez_Blog13 分钟前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
珊珊而川22 分钟前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试
森叶32 分钟前
Electron 安装包 asar 解压定位问题实战
前端·javascript·electron
drebander35 分钟前
ubuntu 安装 chrome 及 版本匹配的 chromedriver
前端·chrome
软件技术NINI1 小时前
html知识点框架
前端·html
深情废杨杨1 小时前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS1 小时前
【vue3】vue3.3新特性真香
前端·javascript·vue.js
markzzw1 小时前
我在 Thoughtworks 被裁前后的经历
前端·javascript·面试