简体   繁体   English

`history.push`的反应路由器竞争条件

[英]React Router race condition of `history.push`

Demo: https://codesandbox.io/s/react-router-basic-forked-mnyw6?file=/example.js演示: https://codesandbox.io/s/react-router-basic-forked-mnyw6?file=/example.js

This is a uber simplification of our case.这是我们案例的超级简化。

  1. Alice lands on /page1/tab1 (click the Start link at top) Alice 登陆 /page1/tab1(点击顶部的 Start 链接)
  2. Alice clicks the "Nav" button. Alice 单击“导航”按钮。 We redirect her to /page2/tab1 (with the hope that there is a tab1 in the page2)我们将她重定向到/page2/tab1 (希望page2中有tab1)
  3. We found tab1 is not available for page2, so we redirect Alice to the default tab tab2 of page2, with the "latest" page value in the state.我们发现 tab1 对 page2 不可用,所以我们将 Alice 重定向到 page2 的默认 tab tab2 ,“最新”页面值在 state 中。 (Expected: page2; Actual: page1) (预期:第 2 页;实际:第 1 页)

The issue is that at the time of the second history.push , the first history.push is not yet done so the page state is still page1 .问题是在第二个history.push的时候,第一个history.push还没有完成,所以page state 仍然是page1 Thus Alice is redirected to /page1/tab2 instead of /page2/tab2 .因此 Alice 被重定向到/page1/tab2而不是/page2/tab2

I want to use the info in the URL as the source of truth so I don't want to store local state of page and tab .我想使用 URL 中的信息作为事实来源,所以我不想存储pagetab的本地 state 。 Instead, I always derive the page and tab from the location.pathname .相反,我总是从location.pathname派生pagetab

In your code, when you create function nav , you are creating it with local variable page .在您的代码中,当您创建 function nav时,您正在使用局部变量page创建它。 React, react router or anything else outside of your component have no idea about this variable. React、react router 或组件之外的任何其他东西都不知道这个变量。 It will never change it's value.它永远不会改变它的价值。 On subsequent render you will create absolutely new local variable with same name and new value.在随后的渲染中,您将创建具有相同名称和新值的全新局部变量。

Best way to handle this is to use declarative Redirect .处理此问题的最佳方法是使用声明性Redirect I forked your sandbox to show you how: https://codesandbox.io/s/react-router-race-condition-forked-c43m2?file=/example.js:595-2068我分叉了你的沙箱,向你展示如何: https://codesandbox.io/s/react-router-race-condition-forked-c43m2?file=/example.js:595-2068

Here is annotated relevant parts:以下是注释的相关部分:

const location = useLocation();
const history = useHistory();
const [page, tab] = location.pathname.split("/").filter((x) => !!x);
// this you will replace with detecting if there is a tab
// I made small map for two pages, and everything else (like home) covered with undefined
// It also detects what tab would be default, so we can redirect
let actualTab = tabs_on_pages[page]?.includes(tab)
  ? tab
  : tabs_on_pages[page]?.[0];
// since you know what tab you want to display at this point, you render Redirect component instead of a page
// This way router will redirect you where you are needed
if (tab !== actualTab) return <Redirect to={`/${page}/${actualTab}`} />;
// Otherwise return tab
return (
  <div>
    <h2>Home</h2>
    <div>{location.pathname}</div>
    <div>Page: {page}</div>
    <div>Tab: {tab}</div>
    <button onClick={nav}>Nav</button>
  </div>
);

If you still want to use imperative way, you will have to enclose not only page into nav , but window.location so after timeout you will retrieve actual value, like this:如果您仍想使用命令式方式,您不仅需要将page包含在nav中,还需要将window.location ,因此在超时后您将检索实际值,如下所示:

setTimeout(() => {
  const [currentPage] = window.location.pathname.split("/").filter((x) => !!x);
  console.debug(currentPage);
  history.push(`/${currentPage}/tab2`);
}, 1);

I advise you not to go with this path, since it is piling up side effects and mixing multiple sources of truth together.我建议您不要使用此路径 go,因为它会堆积副作用并将多个事实来源混合在一起。 Most likely it will bite you in the future.很可能它会在未来咬你。

Answer on comments回复评论

For the page variable, that's just a derived state from the location.pathname, not a react state variable.对于页面变量,这只是从 location.pathname 派生的 state,而不是反应 state 变量。

Assume that values you get from hooks are state variable.假设您从挂钩中获得的值是 state 变量。 You don't know internal implementation, and even if you are - there is a chance that in next update it will change你不知道内部实现,即使你知道 - 有可能在下一次更新中它会改变

May you elaborate on "piling up side effects and mixing multiple sources of truth together"?您能否详细说明“堆积副作用并将多个真相来源混合在一起”?

What are the multiple sources of truth?真相的多重来源是什么?

As stated above, when you get value from useLocation you are accessing state variable.如上所述,当您从useLocation获取值时,您正在访问 state 变量。 If you will get location from window.location , you may get another value, if state inside router not yet updated.如果您将从window.location获得位置,如果路由器内部的 state 尚未更新,您可能会获得另一个值。 Which one of this values you should consider The Truth?您应该考虑以下哪一项价值? From browser point of view it would be value from window.location .从浏览器的角度来看,它将是window.location的值。 From React point of view it would be state variable, because react don't know anything about window and don't track this values.从 React 的角度来看,它将是 state 变量,因为 react 对 window 一无所知,也不跟踪这个值。 React operates with state and only with current values of the state, so it is preferable to use state as a source of truth. React 使用 state 并且仅使用 state 的当前值运行,因此最好使用 state 作为事实来源。 There are ways to make react track some external state, but they still involve using state variables (if you are interested, look how react-redux works - it's very educational).有一些方法可以让反应跟踪一些外部 state,但它们仍然涉及使用 state 变量(如果你有兴趣,看看 react-redux 是如何工作的 - 这很有教育意义)。

It's because I used two side-effects = two history.push?这是因为我使用了两个副作用 = 两个 history.push?

About piling side effects, I meant example with getting value from window.location directly.关于打桩副作用,我的意思是直接从window.location获取值的示例。 In general, you can't do much without side effects.一般来说,你不能做很多没有副作用。 Hooks are, in fact, side effects, but with special properties.挂钩实际上是副作用,但具有特殊属性。 It is normal to use multiple useEffects in component.在组件中使用多个useEffects是正常的。 However, side effects are often hard to debug and trace, especially when different parts of your system do same things (in your case it is your event handler and react router).但是,副作用通常很难调试和跟踪,尤其是当系统的不同部分执行相同的操作时(在您的情况下,它是您的事件处理程序和反应路由器)。 If you add some asynchronous operations (like timeouts) it can become even harder.如果您添加一些异步操作(如超时),它会变得更加困难。 When you use react-router - let router do all hard work of changing history, managing location and notifying react about this.当您使用 react-router 时 - 让路由器完成所有艰苦的工作,包括更改历史、管理位置和通知对此作出反应。 Your goal is to tell react how you want to represent some data.你的目标是告诉 react 你想如何表示一些数据。 In your case, you can do this declaratively with two components: Link to handle user action, and Redirect in case if current state should end up with redirect.在您的情况下,您可以使用两个组件以声明方式执行此操作: Link以处理用户操作,以及Redirect以防当前state 应该以重定向结束。 Side effects are often imperative (ie you write precise instructions about how stuff should work), and goal of react and react-router is to make most of your application declarative (ie describe what you want to get and let react and router do their job).副作用通常是势在必行的(即,您编写有关东西应该如何工作的精确说明),react 和 react-router 的目标是使您的大部分应用程序具有声明性(即描述您想要获得的内容并让 react 和路由器完成它们的工作)。

In short, if you can avoid invoking side effect manually - avoid it.简而言之,如果您可以避免手动调用副作用 - 避免它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM