-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
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(core): make error boundary fallback a component instead of a callback #7368
Conversation
✅ [V2]
To edit notification comments on pull requests, go to your Netlify site settings. |
⚡️ Lighthouse report for the deploy preview of this PR
|
Size Change: +25 B (0%) Total Size: 811 kB ℹ️ View Unchanged
|
This change is not good @Josh-Cena unfortunately This is a bad practice in React to create components inline during the rendering, notably because the component is recreated everytime and then will always unmount/remount. Let's use something similar to our doc example: import React from 'react';
import ErrorBoundary from '@docusaurus/ErrorBoundary';
const SafeComponent = () => (
<ErrorBoundary
fallback={({error, tryAgain}) => (
<div>
<p>This component crashed because of error: {error.message}.</p>
<button onClick={tryAgain}>Try Again!</button>
<SomeStatefulComponent/>
</div>
)}>
<SomeDangerousComponentThatMayThrow />
</ErrorBoundary>
); In this case, everything SafeComponent rerenders, the fallback component is re-created, and Considering there's no easy way to force users to not use inline components, I'd rather move back to the previous implementation based on a callback Using "render callback" remains a good practice in React for some specific cases like this IMHO. For example, React-Native has: <FlatList
renderItem={({ item, index, separators }) => (
<CustomItem item={item}/>
)}
/> |
Previously, it was not a callback either—the default fallback is still a component (imported from I'm okay to make it clearer that it's a callback, not a component, but all these are (1) not fixed by using an unmemoized callback which still gets re-created (2) can be fixed by memoization, either with |
Yes it was a callback, because it was called. The default callback signature may have been confusing because it was declared as
The fact that the callback is recreated on each render is not a problem: as long as it returns the same JSX tree, the rendered components do not unmount/remount. That's not the case with an inline component: React sees it as a different component instance every render.
useMemo is not supposed to be a 100% strong memo guarantee (it is until now, but React may decide later to evict it) and your app shouldn't rely on it to work. Creating components in useMemo should rather be avoided because there's a risk that later React will recreate the component (and thus stateful state is lost again) I wouldn't be surprised if React later decide to evict useMemo more aggressively when using StrictMode for example |
error, | ||
tryAgain: () => this.setState({error: null}), | ||
}); | ||
const Fallback = this.props.fallback ?? DefaultFallback; |
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.
Right, this fix is in place because we had users who swizzled @theme/Error
and used a useState
in it, and that broke, because we were calling the component here (!)
I'm fine to change ?? DefaultFallback
to ?? (props) => <DefaultFallback {...props} />
.
Pre-flight checklist
Motivation
I realized we made a wild mistake: we should render the component instead of call it. The semantics of this prop is very unclear: we said in the docs that it's a callback and we also call it like
fallback({...})
, but theDefaultFallback
and what's actually passed in in our internal usage are both components.As a general principle, components should not be called directly, because (a) it may be a class and (b) it may contain hooks. This aims to make the semantics of this prop clearer by making it receive an actual component.
Test Plan
Testing is a bit hard :(