-
-
Notifications
You must be signed in to change notification settings - Fork 198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: avoid double-render #68
Conversation
BREAKING CHANGE: This removes the `componentStack` in the props given to the `FallbackComponent` and `fallbackRender`
Codecov Report
@@ Coverage Diff @@
## master #68 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 1 1
Lines 36 40 +4
Branches 11 11
=========================================
+ Hits 36 40 +4
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for tackling this and removing the second render 😁
this.setState(initialState) | ||
} | ||
|
||
componentDidCatch(error, info) { | ||
this.props.onError?.(error, info?.componentStack) | ||
this.setState({info}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice
// error to be thrown. | ||
// So we make sure that we don't check the resetKeys on the first call | ||
// of cDU after the error is set | ||
if (error !== null && !this.updatedWithError) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I fully understand why the resetKeys
feature is helpful, or what the purpose of onResetKeysChange
does– but the way you're using the updatedWithError
instance attribute looks safe (since it's only modified during the commit phase or during an imperative reset "action".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's actually quite helpful. Here's an example of it in action: https://react-hooks.netlify.app/6 (It's the Extra Credit 8, which you can play with here: https://react-hooks.netlify.app/isolated/final/06.extra-8.js)
Basically, if there's a property that you anticipate resulting in an error to be thrown, then you can put it in the resetKeys
array and after an error has been thrown, if any values in the array changes then it will be reset automatically.
Here's a gif that demonstrates what this does. With the reset keys, the error boundary is automatically reset when I change the pokemon name:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for elaborating. The concept seems a bit odd to me, but not necessarily in a bad way. It could just be because it's a different way of thinking about using the boundary than would have occurred to me.
I guess it's roughly similar to using an id-based key
or something to completely reset a subtree between view changes.
Anyway 😄 it doesn't look like it would cause problems. (Instance properties can sources of subtle side effects but I think this one looks fine.)
this.props.onResetKeysChange?.(prevProps.resetKeys, resetKeys) | ||
this.setState(initialState) | ||
this.reset() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be calling resetErrorBoundary
? Otherwise you won't call props.onReset
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
props.onReset
will be called only for when this.resetErrorBoundary
is called (presumably the end-user clicked a button you rendered in your fallback that triggered this). props.onResetKeysChange
will be called only for when the reset happened due to a change in the resetKeys
. It's possible these will both be set to the same function, but I think having the distinction could be helpful which is why they're different functions.
I'm open to your feedback on this though. Great opportunity to break things since we'll be pushing a major version bump.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. I see what you mean, but I would have assumed (if I were using this component) that onReset
was called any time the boundary reset (for any reason) and onResetKeysChange
was called any time the reset keys changed and those things are decoupled (because one can happen without the other).
How do you actually observe when a reset happens because of a key change? (Using onResetKeysChange
won't be sufficient to tell you this, will it)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you actually observe
Is "you" referring to the developer user or library author? 😅
For the developer, they'll know because onResetKeysChange
will be called. The lib author knows because we do this check in cDU to compare the previous resetKeys
with the next ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was referring to the library user.
And, right. I forgot that we only check and call onResetKeysChange
if the error !== null
(because intuitively, I still think keys changing is separate from resetting).
Anyway, maybe this isn't a valuable conversation to continue. It just seemed a little unintuitive to me, but I'm only one person.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's quite possible this particular feature is a bit niche. But it's definitely solved real problems for me so that's why I think it's worth the added complexity 😅
Thanks for your feedback @bvaughn! Always appreciate your time :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking the time to review this and provide feedback.
// error to be thrown. | ||
// So we make sure that we don't check the resetKeys on the first call | ||
// of cDU after the error is set | ||
if (error !== null && !this.updatedWithError) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's actually quite helpful. Here's an example of it in action: https://react-hooks.netlify.app/6 (It's the Extra Credit 8, which you can play with here: https://react-hooks.netlify.app/isolated/final/06.extra-8.js)
Basically, if there's a property that you anticipate resulting in an error to be thrown, then you can put it in the resetKeys
array and after an error has been thrown, if any values in the array changes then it will be reset automatically.
Here's a gif that demonstrates what this does. With the reset keys, the error boundary is automatically reset when I change the pokemon name:
this.props.onResetKeysChange?.(prevProps.resetKeys, resetKeys) | ||
this.setState(initialState) | ||
this.reset() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
props.onReset
will be called only for when this.resetErrorBoundary
is called (presumably the end-user clicked a button you rendered in your fallback that triggered this). props.onResetKeysChange
will be called only for when the reset happened due to a change in the resetKeys
. It's possible these will both be set to the same function, but I think having the distinction could be helpful which is why they're different functions.
I'm open to your feedback on this though. Great opportunity to break things since we'll be pushing a major version bump.
🎉 This PR is included in version 3.0.0 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
Great, now how do I get the component stack? It was essential for me. |
Use the old version or use the onError handler. Or you could write your own error boundary. |
@ranisalt This component is very small. If you have a use case it doesn't solve- fork it. |
I see. A combination of |
I noticed that when using `react-error-boundary@2` (without bvaughn/react-error-boundary#68), and fixing my oversights from the error boundary PR, they work properly with the intended `resetKeys` prop instead of `key`, and we thus avoid the tree unmount issue that `key` introduced.
I noticed that when using `react-error-boundary@2` (without bvaughn/react-error-boundary#68), and fixing my oversights from the error boundary PR, they work properly with the intended `resetKeys` prop instead of `key`, and we thus avoid the tree unmount issue that `key` introduced.
@ranisalt how exactly did you do this? Is it similar to the code below from this gist? export default function MyApp({ Component, pageProps }: { Component: any; pageProps: any }) {
// ReactErrorBoundary doesn't pass in the component stack trace.
// Capture that ourselves to pass down via render props
const [errorInfo, setErrorInfo] = useState<React.ErrorInfo | null>(null);
return (
<React.Fragment>
<ErrorBoundary
onError={(error, info) => {
if (process.env.NODE_ENV === 'production') {
uploadErrorDetails(error, info);
}
setErrorInfo(info);
}}
fallbackRender={fallbackProps => {
return <AppErrorFallback {...fallbackProps} errorInfo={errorInfo} />;
}}
>
<Component {...pageProps} />;
</ErrorBoundary>
</React.Fragment>
);
} |
@bvaughn @kentcdodds would you be open to having an example like this in the readme for those who want the component stack trace? Maybe with the caveat that it would cause a double render? |
That's fine with me. Most of the time the component stack trace is unnecessary, so make a note about that as well 👍 |
This commits twice on error right? Once with the current @bvaughn, @kentcdodds Will this potentially render a transient invalid state? |
What: fix: avoid double-render
Why: Closes #66 and Closes #67
How:
setState
call incomponentDidCatch
Checklist:
BREAKING CHANGE: This removes the
componentStack
in the props given to theFallbackComponent
andfallbackRender