Error Boundaries disables routing inside of a Swit

2020-03-30 08:21发布

For a long time I have been trying to get routing to work in our app after an error boundary has been hit, but only today did I find the code that was seemingly identical to the many examples lying around had one important difference: the routes were wrapped by a Switch. This simple change is enough to stop routing from working, if enabled. Demo

Take the following snippet. If I remove the Switch bit, this works fine even if each component should fail, but not if wrapped by the switch. I would like to know why.

<div style={{ backgroundColor: "#ffc993", height: "150px" }}>
<Switch>
  <Route
    path="/"
    exact
    render={() => (
      <ErrorBoundary>
        <MyComponent1 title="Component 1" />
      </ErrorBoundary>
    )}
  />
  <Route
    path="/comp1"
    render={() => (
      <ErrorBoundary>
        <MyComponent1 title="Component 1 Again" />
      </ErrorBoundary>
    )}
  />
  <Route
    path="/comp2"
    render={() => (
      <ErrorBoundary>
        <MyComponent2 title="Component 2" />
      </ErrorBoundary>
    )}
  />
</Switch>

2条回答
可以哭但决不认输i
2楼-- · 2020-03-30 08:50

Basically, this problem boils down to how React does reconciliation.

When a component updates, the instance stays the same, so that state is maintained across renders. React updates the props of the underlying component instance to match the new element

Say we have this example app:

<App>
  <Switch>
    <Route path="a" component={Foo}/>
    <Route path="b" component={Foo}/>
  </Switch>
</App> 

This will, somewhat unintuitively, reuse the same instance of Foo for both routes! A <Switch> will always return the first matched element, so basically when React renders this is equivalent of the tree <App><Foo/></App> for path "a" and <App><Foo/></App> for path "b". If Foo is a component with state, that means that state is kept, as the instance is just passed new props (for which there are none, except children, in our case), and is expected to handle this by recomputing its own state.

As our error boundary is being reused, while it has state that has no way of changing, it will never re-render the new children of its parent route.

React has one trick hidden up its sleeve for this, which I have only seen explicitly documented on its blog:

In order to reset the value when moving to a different item (as in our password manager scenario), we can use the special React attribute called key. When a key changes, React will create a new component instance rather than update the current one. (...) In most cases, this is the best way to handle state that needs to be reset.

I was first hinted to this by a somewhat related issue on Brian Vaughn's error bondary package:

The way I would recommend resetting this error boundary (if you really want to blow away the error) would be to just clear it out using a new key value. (...) This will tell React to throw out the previous instance (with its error state) and replace it with a new instance.

The alternative to using keys would be to implement either exposing some hook that could be called externally or by trying to inspect the children property for change, which is hard. Something like this could work (demo):

componentDidUpdate(prevProps, prevState, snapshot) {
    const childNow = React.Children.only(this.props.children);
    const childPrev = React.Children.only(prevProps.children);

    if (childNow !== childPrev) {
        this.setState({ errorInfo: null });
   }

But it's more work and much more error prone, so why bother: just stick to adding a key prop :-)

查看更多
霸刀☆藐视天下
3楼-- · 2020-03-30 09:00

To give you the shortcut of this fix, please see the new "key" prop on each of the ErrorBoundary component and each must be unique, so the code should look like this:

<Switch>
  <Route
    path="/"
    exact
    render={() => (
      <ErrorBoundary key="1">
        <MyComponent1 title="Component 1" />
      </ErrorBoundary>
    )}
  />
  <Route
    path="/comp1"
    render={() => (
      <ErrorBoundary key="2">
        <MyComponent1 title="Component 1 Again" />
      </ErrorBoundary>
    )}
  />
  <Route
    path="/comp2"
    render={() => (
      <ErrorBoundary key="3">
        <MyComponent2 title="Component 2" />
      </ErrorBoundary>
    )}
  />
</Switch>

To elaborate, the answer of @oligofren is correct. Those 3 ErrorBoundary components are the same instances but may differ in props. You can verify this by passing "id" prop to each of the ErrorBoundary components.

Now you mentioned why is that if you remove Switch component, it works as expected? Because of this code: https://github.com/ReactTraining/react-router/blob/e81dfa2d01937969ee3f9b1f33c9ddd319f9e091/packages/react-router/modules/Switch.js#L40

I recommend you to read the official documentation of React.cloneElement here: https://reactjs.org/docs/react-api.html#cloneelement

I hope this gives you an idea on this issue. Credit to @oligofren as he explained in more details about the idea of the instances of those components.

查看更多
登录 后发表回答