在React-router中拦截/处理浏览器的返回按钮?

100

我正在使用 Material-ui 的标签页(Tabs),它们是可控制的。我将它们用于 (React-router) 链接,如下所示:

    <Tab value={0} label="dashboard" containerElement={<Link to="/dashboard/home"/>}/>
    <Tab value={1} label="users" containerElement={<Link to="/dashboard/users"/>} />
  <Tab value={2} label="data" containerElement={<Link to="/dashboard/data"/>} />

如果我当前正在访问dashboard/data,然后我单击浏览器的后退按钮,我会进入(例如)dashboard/users,但是突出显示的选项卡仍然停留在dashboard/data(value=2)

我可以通过设置状态来更改,但我不知道如何处理浏览器后退按钮被按下时的事件?

我找到了这个:

window.onpopstate = this.onBackButtonEvent;

但是每次状态变化时都会调用它(不仅在后退按钮事件上)。


1
我知道现在有点晚了,但是在 useEffect 中使用空参数调用它,像这样:useEffect(()=>{/* 这里 */}, []) - Hyzyr
17个回答

63

使用react-router使得工作变得简单,如下:

import { browserHistory } from 'react-router';

componentDidMount() {
    this.onScrollNearBottom(this.scrollToLoad);

    this.backListener = browserHistory.listen((loc, action) => {
      if (action === "POP") {
        // Do your stuff
      }
    });
  }

componentWillUnmount() {
    // Unbind listener
    this.backListener();
}

@loopmode 当你处理返回按钮操作时,你会使用类似 browserHistory.goBack() 这样的东西吗? - Jay Shin
3
我发现在其他地方,一位同事正在手动干预历史记录,大致是这样的:如果(nextLocation.action === 'POP' && getStepIndex(nextLocation.pathname) === 0) { browserHistory.push({pathname: ${getPathForIndex(0)}}); return false; }所以..如果“后退”则“前进”,以使前进按钮无法使用(因为它会在未提交表单的情况下前进)。所以..您的答案仍然是正确的-是我自己犯的用户错误 :) - loopmode
5
react-router@5.0.0开始,您不能像这个回答中所示那样导入browserHistory。看起来history包含在从路由引用的任何组件传递的props中。如果我不太正确,请随意纠正。 - Stoph
7
对于使用 React Router 4+ 的任何人来说,listen 方法有两个参数:location 和 action。history.listen((loc, action) => if (action === 'POP') // do stuff)。如果 action 等于 'POP',那么执行一些操作。 - mawburn
1
我该如何防止 POP 被执行呢? - Paulo
显示剩余2条评论

54

使用钩子函数,您可以检测到浏览器的后退和前进按钮。

import { useHistory } from 'react-router-dom'


const [ locationKeys, setLocationKeys ] = useState([])
const history = useHistory()

useEffect(() => {
  return history.listen(location => {
    if (history.action === 'PUSH') {
      setLocationKeys([ location.key ])
    }

    if (history.action === 'POP') {
      if (locationKeys[1] === location.key) {
        setLocationKeys(([ _, ...keys ]) => keys)

        // Handle forward event

      } else {
        setLocationKeys((keys) => [ location.key, ...keys ])

        // Handle back event

      }
    }
  })
}, [ locationKeys, ])

2
在 setLocationKeys(([ _, ...keys ]) => keys) 中,下划线(_)代表一个占位符,用于表示我们不关心的变量或值。它通常用于忽略某些不需要使用的参数或元素。在这个例子中,我们只对剩余的键(keys)感兴趣,而不关心第一个元素。 - Shabbir Essaji
3
@ShabbirEssaji 这段代码使用了解构赋值和扩展运算符,以返回一个去除第一个元素的数组。这篇文章可能有所帮助:https://codeburst.io/a-simple-guide-to-destructuring-and-es6-spread-operator-e02212af5831 - rob-gordon
2
@ShabbirEssaji,你可能已经找到了答案,但是当变量必须被赋值但不会被使用时,下划线通常会按照惯例使用。 - Josh J
这种处理前进和后退的方式仍然是好的吗?或者是否已经添加了任何内容来处理可说是有些糟糕的“locationKeys,setLocationKeys”部分?还没有。谢谢。 - Joseph Beuys' Mum
到目前为止,是的,这是我们拥有的最佳解决方案。 - Nicolas Keller
显示剩余2条评论

24

这是我最终的做法:

componentDidMount() {
    this._isMounted = true;
    window.onpopstate = ()=> {
      if(this._isMounted) {
        const { hash } = location;
        if(hash.indexOf('home')>-1 && this.state.value!==0)
          this.setState({value: 0})
        if(hash.indexOf('users')>-1 && this.state.value!==1)
          this.setState({value: 1})
        if(hash.indexOf('data')>-1 && this.state.value!==2)
          this.setState({value: 2})
      }
    }
  }

谢谢大家帮忙,哈哈


23
不是React的方式。 - Atombit
8
“React 方式” 过于严格和复杂。 - Emperor Eto
如果您使用Next.js,则以下是唯一有效的解决方案。原生的Next.js路由事件监听器无法在浏览器中看到“返回”点击。 - dimaninc
有没有使用 Next.js 中的钩子 useEffect 来实现这个的方法? - Smit Patel

24

Hooks 示例

const {history} = useRouter();
  useEffect(() => {
    return () => {
      // && history.location.pathname === "any specific path")
      if (history.action === "POP") {
        history.replace(history.location.pathname, /* the new state */);
      }
    };
  }, [history])

我不使用history.listen,因为它不会影响状态。

const disposeListener = history.listen(navData => {
        if (navData.pathname === "/props") {
            navData.state = /* the new state */;
        }
    });

2
我会将依赖数组更改为[history.location,history.action],因为它无法捕获位置更改。 - anerco
1
useRouter() 不是特定于 Next.js 框架吗? - Andrew
我只能在useHooks库中找到useRouter()函数:https://usehooks.com/useRouter/ - mheavers

13

这个问题的大多数答案要么使用过时的React Router版本,要么依赖于不太现代的Class组件,或者令人困惑; 而且没有一个使用常见的TypeScript组合。下面是一个使用Router v5、函数组件和Typescript的答案:

// use destructuring to access the history property of the ReactComponentProps type
function MyComponent( { history }: ReactComponentProps) {

    // use useEffect to access lifecycle methods, as componentDidMount etc. are not available on function components.
    useEffect(() => {

        return () => {
            if (history.action === "POP") {
                // Code here will run when back button fires. Note that it's after the `return` for useEffect's callback; code before the return will fire after the page mounts, code after when it is about to unmount.
                }
           }
    })
}

这里可以找到一个带有解释的更完整的示例,点击这里


9

React Router API的3.x版本有一组实用工具,您可以使用这些工具在事件注册到浏览器历史记录之前公开“返回”按钮事件。您必须首先将您的组件包装在withRouter()高阶组件中。然后,您可以使用setRouteLeaveHook()函数,该函数接受任何带有有效path属性和回调函数的route对象。

import {Component} from 'react';
import {withRouter} from 'react-router';

class Foo extends Component {
  componentDidMount() {
    this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave);
  }

  routerWillLeave(nextState) { // return false to block navigation, true to allow
    if (nextState.action === 'POP') {
      // handle "Back" button clicks here
    }
  }
}

export default withRouter(Foo);

对我来说,它只是给出了一个错误 TypeError: Cannot read property 'setRouteLeaveHook' of undefined - Nikita Vlasenko
@NikitaVlasenko 在上面的例子中,需要将 Foo 传递给 <Route /> 组件,或者至少需要继承路由组件的 props。(例如,在您的 routes.js 文件中,<Route component={Foo}>/* ... */</Route> - brogrammer

8

使用钩子函数。我已将@Nicolas Keller的代码转换为TypeScript。

  const [locationKeys, setLocationKeys] = useState<(string | undefined)[]>([]);
  const history = useHistory();

  useEffect(() => {
    return history.listen((location) => {
      if (history.action === 'PUSH') {
        if (location.key) setLocationKeys([location.key]);
      }

      if (history.action === 'POP') {
        if (locationKeys[1] === location.key) {
          setLocationKeys(([_, ...keys]) => keys);

          // Handle forward event
          console.log('forward button');
        } else {
          setLocationKeys((keys) => [location.key, ...keys]);

          // Handle back event
          console.log('back button');
          removeTask();
        }
      }
    });
  }, [locationKeys]);

1
_ 做什么? - KshitijV97
1
@KshitijV97 这意味着“忽略此变量”,此外,可以使用“_”的约定,例如“_firstElement”,即使未使用此“_firstElement”,也不会导致任何警告/错误。 - Michael Harley

5
在 NextJs 中,我们可以使用 beforePopState 函数并实现我们想要的功能,例如关闭模态框、展示模态框或检查后退地址并决定下一步操作。
const router = useRouter();

useEffect(() => {
    router.beforePopState(({ url, as, options }) => {
        // I only want to allow these two routes!

        if (as === '/' ) {
            // Have SSR render bad routes as a 404.
             window.location.href = as;
            closeModal();
            return false
        }

        return true
    })
}, [])

你对"Forward"按钮也有解决方案吗? - Or Nakash

4

我使用了withrouter高阶组件,以获取history属性,并只写了一个componentDidMount()方法:

componentDidMount() {
    if (this.props.history.action === "POP") {
        // custom back button implementation
    }
}

1
它在点击返回按钮之前触发,你能帮忙吗?我们需要在点击返回按钮后触发自定义弹出窗口。 - 151291

3
在您的componentDidMount()中添加这两行代码。这对我有用。
window.history.pushState(null, null, document.URL);
window.addEventListener('popstate', function(event) {
      window.location.replace(
        `YOUR URL`
      );
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接