fix(overrides): upsell footer not rendering in date filter - #120673
Conversation
| // Overrides are registered asynchronously during app bootstrap; | ||
| // resolving too early can permanently miss them. | ||
| let ResolvedComponent: React.ComponentType<any> | undefined; | ||
| let isResolved = false; |
There was a problem hiding this comment.
isResolved seems unneeded. It'll always be set to true whenever ResolvedComponent is set.
But i don't know if this fixes the issue. The code before #120028 would call getOverride(overrideName)?.() ?? getDefaultComponent() fresh on each render. This will still call it early/once and cache the result it seems
| const OverrideComponent: React.ComponentType<any> = | ||
| getOverride(overrideName)?.() ?? getDefaultComponent(); |
There was a problem hiding this comment.
Bug: The getDefaultComponent function calls React.lazy() on every render if defaultComponentPromise is used, which is a React anti-pattern that can cause infinite loops.
Severity: LOW
Suggested Fix
The React.lazy() call should be moved outside the render path. Create the lazy component once when OverrideOrDefault is initially called and store it, rather than re-creating it inside the getDefaultComponent function on every render. This ensures the same component instance is used across renders.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: static/app/components/overrideOrDefault.tsx#L67-L68
Potential issue: The `OverrideOrDefaultComponent` calls `getDefaultComponent()` on every
render. If the `defaultComponentPromise` prop is provided, `getDefaultComponent` will in
turn call `React.lazy(defaultComponentPromise)` on every render. This creates a new,
distinct lazy component instance each time the component renders, which is a known React
anti-pattern. This can lead to an infinite re-rendering loop when used with
`<Suspense>`. While no current code in the repository uses the `defaultComponentPromise`
parameter, the API allows it, creating a latent bug that could be triggered by future
code changes.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9382bc6. Configure here.
| } | ||
|
|
||
| return <ResolvedComponent {...props} />; | ||
| return <OverrideComponent {...props} />; |
There was a problem hiding this comment.
Default lazy remounts every render
Medium Severity
Deferring getOverride to render fixes the upsell timing issue, but getDefaultComponent() is also invoked on every render. When defaultComponentPromise is set, that path builds a new lazy() component and wrapper each time, so React treats it as a new type and remounts the subtree (state reset, Suspense flicker). Only the override lookup needs to be late-bound; the default can stay resolved once outside render.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 9382bc6. Configure here.
There was a problem hiding this comment.
Can you make a variable to cache this so it's stored at the module level and won't re-evaluate unnecessarily every render?
e.g. loosely depicted
let DefaultComponent: React.ComponentType<any>;
export function OverrideOrDefault({ ... }) {
function getDefaultComponent(): React.ComponentType<any> | undefined {
...
if (defaultComponentPromise) {
if (DefaultComponent) {
return DefaultComponent;
}
// Lazy adds a complicated type that is not important
DefaultComponent = lazy(defaultComponentPromise);
return function (props: Props) {
return (
<Suspense fallback={null}>
<DefaultComponent {...props} />
</Suspense>
);
};
}
return defaultComponent;
}
}
📊 Type Coverage Diff
🔍 1 new type safety issue introduced
This is informational only and does not block the PR. |
narsaynorath
left a comment
There was a problem hiding this comment.
Just left a comment trying to mitigate the bot comments on the PR. I'm not too familiar with the component so if you need a deeper dive, let me know, but if this is mostly a revert then it looks good to me
| } | ||
|
|
||
| return <ResolvedComponent {...props} />; | ||
| return <OverrideComponent {...props} />; |
There was a problem hiding this comment.
Can you make a variable to cache this so it's stored at the module level and won't re-evaluate unnecessarily every render?
e.g. loosely depicted
let DefaultComponent: React.ComponentType<any>;
export function OverrideOrDefault({ ... }) {
function getDefaultComponent(): React.ComponentType<any> | undefined {
...
if (defaultComponentPromise) {
if (DefaultComponent) {
return DefaultComponent;
}
// Lazy adds a complicated type that is not important
DefaultComponent = lazy(defaultComponentPromise);
return function (props: Props) {
return (
<Suspense fallback={null}>
<DefaultComponent {...props} />
</Suspense>
);
};
}
return defaultComponent;
}
}

Just fixing a bug i've noticed! I git blamed this pr to being the culprit but basically the date filter upsell footer was not rendering because it was being forced to resolve before the async call could return the component. This change waits for that async call to return before deciding if it has been resolved or not.
EDIT: Talked to Ryan in DMs and decided to revert sentry's changes so i've just edited the code for that