-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(core): Better handling for nested frames #10713
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
base: main
Are you sure you want to change the base?
fix(core): Better handling for nested frames #10713
Conversation
7cca393
to
c55ab1f
Compare
5f86bc7
to
6e86af2
Compare
View your CI Pipeline Execution ↗ for commit 211f28f.
☁️ Nx Cloud last updated this comment at |
a9d0902
to
67ce09a
Compare
@NathanWalker This should be good to go. Let me say that theoritically, this PR doesn't completely solve the problem because we have some UPDATED: This has actually got more room for improvements. |
It seems the problem goes a bit deeper so I'll keep this one as draft for just a little. |
@coderabbitai please review |
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain@NathanWalker I'll review this PR for you. Let me gather the details about the changes first. 🏁 Script executed:
Length of output: 23664 Review of PR #10713: Better handling for nested frames@NathanWalker This PR addresses a critical issue with nested frame management in NativeScript core. Here's my comprehensive review: 🎯 Core Problem & SolutionThe PR tackles the "No view found for id" exception that occurs when nested frames are destroyed and reused, particularly evident in Vue applications. The solution introduces proper sequencing of frame loading through an event-driven approach. ✅ Architectural Improvements
|
This comment was marked as resolved.
This comment was marked as resolved.
@coderabbitai review |
✅ Actions performedReview triggered.
|
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.
Actionable comments posted: 5
🧹 Nitpick comments (4)
apps/automated/src/ui/frame/frame-tests-common.ts (1)
272-320
: Well-structured test for frame loading coordination.The test effectively validates the nested frame loading order, which is crucial for the PR's objectives. The test methodology is sound:
- Creates root and nested frames in proper hierarchy
- Overrides
onFrameLoaded
to track execution order- Validates root frame loads before nested frame
- Proper cleanup of overrides and frame stack
However, consider a few improvements:
Consider adding error handling and more detailed assertions:
+ // Ensure we have exactly 2 frames loaded + TKUnit.assertEqual(loadedFrames.length, 2, "Expected exactly 2 frames to be loaded"); + TKUnit.assertEqual(rootFrame, loadedFrames[0]); TKUnit.assertEqual(nestedFrame, loadedFrames[1]); + + // Verify frames are in expected loaded state + TKUnit.assertTrue(rootFrame.isLoaded, "Root frame should be loaded"); + TKUnit.assertTrue(nestedFrame.isLoaded, "Nested frame should be loaded");Also consider testing error scenarios where frames might fail to load properly.
packages/core/ui/frame/frame-common.ts (1)
356-361
: Missing documentation for new internal event
_notifyFrameEntryLoaded()
emits_frameEntryLoaded
, but neither the JSDoc nor public typings mention the event.
Please add a short comment so maintainers know this is internal and avoid subscribing from user code.packages/core/ui/frame/fragment.transitions.android.ts (2)
436-444
: Cloned transition listeners keep strong reference to originalentry
cloneExpandedTransitionListener
reusesexpandedTransitionListener.entry
, so every clone still references the old entry object.
That is intended for restore, but if the snapshot outlives the entry’s fragment (e.g. orientation change) the listener keeps the fragment from GC.If unavoidable, at least wrap
entry
in aWeakRef
inside the listener, or ensure_restoreTransitionState
executes quickly and the snapshot is nulled afterwards (see previous comment).
155-156
: String slicing duplicated & fragile
'flip'.length
/'slide'.length
string-math is repeated three times.
A simple helper improves readability and avoids mismatched literals:function stripPrefix(value: string, prefix: string) { return value.startsWith(prefix) ? value.substring(prefix.length) : value; } const direction = stripPrefix(name, 'flip') || 'right';Also applies to: 628-629, 653-654
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
apps/automated/src/ui/frame/frame-tests-common.ts
(2 hunks)packages/core/ui/core/view-base/index.ts
(4 hunks)packages/core/ui/frame/callbacks/activity-callbacks.ts
(0 hunks)packages/core/ui/frame/fragment.transitions.android.ts
(7 hunks)packages/core/ui/frame/fragment.transitions.d.ts
(2 hunks)packages/core/ui/frame/frame-common.ts
(4 hunks)packages/core/ui/frame/index.android.ts
(7 hunks)packages/core/ui/frame/index.ios.ts
(2 hunks)
💤 Files with no reviewable changes (1)
- packages/core/ui/frame/callbacks/activity-callbacks.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
packages/core/ui/frame/fragment.transitions.d.ts (1)
packages/core/ui/frame/fragment.transitions.android.ts (2)
_getTransitionState
(487-504)_restoreTransitionState
(506-526)
packages/core/ui/frame/index.ios.ts (1)
packages/core/ui/frame/index.d.ts (2)
BackstackEntry
(454-484)NavigationTransition
(415-449)
packages/core/ui/frame/index.android.ts (3)
packages/core/ui/frame/fragment.transitions.android.ts (3)
_getTransitionState
(487-504)_updateTransitions
(283-309)_restoreTransitionState
(506-526)packages/core/ui/frame/frame-common.ts (2)
currentEntry
(559-565)BackstackEntry
(18-18)packages/core/ui/frame/index.d.ts (1)
BackstackEntry
(454-484)
🪛 Biome (1.9.4)
packages/core/ui/frame/frame-common.ts
[error] 84-84: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🔇 Additional comments (11)
packages/core/ui/core/view-base/index.ts (4)
120-127
: LGTM! Excellent type safety improvement.The generic enhancement to
getAncestor
provides better type safety by allowing the return type to be inferred from the criterion parameter. The type guard approach ensures proper type narrowing.
349-349
: Good addition for subview loading coordination.The private
_isLoadingSubviews
field supports the new event-driven frame loading mechanism described in the PR objectives.
642-644
: Proper encapsulation of loading state.The public getter provides read-only access to the internal loading state, which is appropriate for the frame coordination mechanism.
706-714
: Verify thread safety and exception handling.The loading state tracking implementation looks correct, but consider potential edge cases:
- Exception handling: If an exception occurs while loading child views, the flag might remain
true
indefinitely- Re-entrancy: Consider what happens if
onLoaded
is called recursively during child loadingConsider wrapping the child loading logic in a try-finally block to ensure the flag is always reset:
this._isLoadingSubviews = true; - -this.eachChild((child) => { - this.loadView(child); - - return true; -}); - -this._isLoadingSubviews = false; +try { + this.eachChild((child) => { + this.loadView(child); + return true; + }); +} finally { + this._isLoadingSubviews = false; +}packages/core/ui/frame/fragment.transitions.d.ts (2)
1-1
: LGTM! Import added for new transition state functionality.The
TransitionState
import is correctly added to support the new transition state management functions.
23-30
: Good modularization of transition state management.The new function declarations provide a clean API for saving and restoring transition states, which should help with the nested frame lifecycle issues mentioned in the PR objectives.
The function signatures are appropriate:
_getTransitionState
returns a snapshot of the current state_restoreTransitionState
accepts a snapshot to restoreapps/automated/src/ui/frame/frame-tests-common.ts (1)
8-8
: LGTM! Import added for test requirements.The
StackLayout
import is correctly added to support the new nested frame test.packages/core/ui/frame/index.ios.ts (2)
91-91
: LGTM! Type safety improvement for navigation core.Changing the parameter type from
any
toBackstackEntry
provides better type safety and aligns with the Android implementation improvements in this PR.
520-520
: LGTM! Refined callback type annotation.The change from generic
Function
to specific() => void
improves type safety by explicitly indicating the callback takes no parameters and returns void. This is more precise than the genericFunction
type.packages/core/ui/frame/index.android.ts (1)
541-565
: Dispose transition snapshot eagerly
disposeNativeView
sets_cachedTransitionState = null
but does not remove any listeners that might have been cloned inside the snapshot.
IfdisposeNativeView
fires before the simulated navigation completes, the cloned transitions may still point to disposed fragments.Consider invoking
_clearEntry(_cachedTransitionState?.entry)
before nullifying the snapshot.packages/core/ui/frame/fragment.transitions.android.ts (1)
283-289
: Good safeguardEarly-returning when
fragment
is falsy prevents NPEs during fragment restoration. Nice catch.
@profile | ||
public onLoaded() { | ||
// Property page refers to the page this frame is nested into | ||
const parentFrame = this.page?.frame; | ||
|
||
super.onLoaded(); | ||
|
||
if (parentFrame?.isLoadingSubviews) { | ||
const frameRef = new WeakRef(this); | ||
|
||
parentFrame.once(FRAME_ENTRY_LOADED_EVENT, () => { | ||
const frame = frameRef.deref(); | ||
if (frame) { | ||
frame.onFrameLoaded(); | ||
} | ||
}); | ||
} else { | ||
this.onFrameLoaded(); | ||
} | ||
} | ||
|
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.
💡 Verification agent
🧩 Analysis chain
Potential dangling listener on parent frame
onLoaded
registers a once
handler on parentFrame
but never stores a reference to that frame.
If the child frame is unloaded before the parent fires _frameEntryLoaded
, that handler remains attached to the parent for the lifetime of the app, causing a memory leak.
Consider capturing parentFrame
in a weak ref and detaching the listener from the parent inside onUnloaded
(or when the child is destroyed).
// capture
const parentFrameListener = () => { … }
parentFrame.once(FRAME_ENTRY_LOADED_EVENT, parentFrameListener);
this._parentFrameRef = new WeakRef(parentFrame);
this._parentFrameListener = parentFrameListener;
and later:
public override onUnloaded() {
…
- if (this.hasListeners(FRAME_ENTRY_LOADED_EVENT)) {
- this.off(FRAME_ENTRY_LOADED_EVENT);
- }
+ const parent = this._parentFrameRef?.deref?.();
+ if (parent && this._parentFrameListener) {
+ parent.off(FRAME_ENTRY_LOADED_EVENT, this._parentFrameListener);
+ }
}
I’d like to confirm whether onUnloaded
is implemented in this class (and that parent.off
is available), so we can determine if the suggested cleanup hook is feasible:
🏁 Script executed:
#!/bin/bash
# Locate frame-common.ts and inspect for onUnloaded override
file=$(fd frame-common.ts)
echo "Inspecting: $file"
rg -n "onUnloaded" -C3 "$file"
# Check the FRAME_ENTRY_LOADED_EVENT declaration for context
rg -n "FRAME_ENTRY_LOADED_EVENT" -C3 "$file"
Length of output: 1658
Fix dangling listener on parent frame
The onLoaded
method registers a once
listener on parentFrame
, but the existing onUnloaded
only calls this.off
on the child frame—leaving the parent-attached callback alive if the child is unloaded first. To prevent this leak, capture both the parent reference and the listener callback, then remove it from the parent in onUnloaded
.
• File: packages/core/ui/frame/frame-common.ts
– onLoaded (lines ~131–139): store parentFrame
and the listener in class fields
– onUnloaded (lines 146–153): call parent.off(FRAME_ENTRY_LOADED_EVENT, listener)
Suggested diff:
diff --git a/packages/core/ui/frame/frame-common.ts b/packages/core/ui/frame/frame-common.ts
index …
--- a/packages/core/ui/frame/frame-common.ts
+++ b/packages/core/ui/frame/frame-common.ts
@@ class FrameCommon {
+ private _parentFrameRef?: WeakRef<FrameCommon>;
+ private _frameLoadListener?: () => void;
@profile
public onLoaded() {
super.onLoaded();
const parentFrame = this.page?.frame;
if (parentFrame?.isLoadingSubviews) {
- const frameRef = new WeakRef(this);
- parentFrame.once(FRAME_ENTRY_LOADED_EVENT, () => {
- const frame = frameRef.deref();
- if (frame) {
- frame.onFrameLoaded();
- }
- });
+ this._parentFrameRef = new WeakRef(parentFrame);
+ const frameRef = new WeakRef(this);
+ this._frameLoadListener = () => {
+ const frame = frameRef.deref();
+ if (frame) {
+ frame.onFrameLoaded();
+ }
+ };
+ parentFrame.once(FRAME_ENTRY_LOADED_EVENT, this._frameLoadListener);
} else {
this.onFrameLoaded();
}
@@
@profile
public onUnloaded() {
super.onUnloaded();
- // This is a precaution in case the method is called asynchronously during the loading procedure
- if (this.hasListeners(FRAME_ENTRY_LOADED_EVENT)) {
- this.off(FRAME_ENTRY_LOADED_EVENT);
- }
+ const parent = this._parentFrameRef?.deref();
+ if (parent && this._frameLoadListener) {
+ parent.off(FRAME_ENTRY_LOADED_EVENT, this._frameLoadListener);
+ this._frameLoadListener = undefined;
+ this._parentFrameRef = undefined;
+ }
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
@profile | |
public onLoaded() { | |
// Property page refers to the page this frame is nested into | |
const parentFrame = this.page?.frame; | |
super.onLoaded(); | |
if (parentFrame?.isLoadingSubviews) { | |
const frameRef = new WeakRef(this); | |
parentFrame.once(FRAME_ENTRY_LOADED_EVENT, () => { | |
const frame = frameRef.deref(); | |
if (frame) { | |
frame.onFrameLoaded(); | |
} | |
}); | |
} else { | |
this.onFrameLoaded(); | |
} | |
} | |
--- a/packages/core/ui/frame/frame-common.ts | |
+++ b/packages/core/ui/frame/frame-common.ts | |
@@ class FrameCommon { | |
+ private _parentFrameRef?: WeakRef<FrameCommon>; | |
+ private _frameLoadListener?: () => void; | |
@profile | |
public onLoaded() { | |
- // Property page refers to the page this frame is nested into | |
- const parentFrame = this.page?.frame; | |
- | |
- super.onLoaded(); | |
+ super.onLoaded(); | |
+ // Property page refers to the page this frame is nested into | |
+ const parentFrame = this.page?.frame; | |
if (parentFrame?.isLoadingSubviews) { | |
- const frameRef = new WeakRef(this); | |
- | |
- parentFrame.once(FRAME_ENTRY_LOADED_EVENT, () => { | |
- const frame = frameRef.deref(); | |
- if (frame) { | |
- frame.onFrameLoaded(); | |
- } | |
- }); | |
+ this._parentFrameRef = new WeakRef(parentFrame); | |
+ const frameRef = new WeakRef(this); | |
+ this._frameLoadListener = () => { | |
+ const frame = frameRef.deref(); | |
+ if (frame) { | |
+ frame.onFrameLoaded(); | |
+ } | |
+ }; | |
+ parentFrame.once(FRAME_ENTRY_LOADED_EVENT, this._frameLoadListener); | |
} else { | |
this.onFrameLoaded(); | |
} | |
@@ | |
@profile | |
public onUnloaded() { | |
super.onUnloaded(); | |
- | |
- // This is a precaution in case the method is called asynchronously during the loading procedure | |
- if (this.hasListeners(FRAME_ENTRY_LOADED_EVENT)) { | |
- this.off(FRAME_ENTRY_LOADED_EVENT); | |
- } | |
+ const parent = this._parentFrameRef?.deref(); | |
+ if (parent && this._frameLoadListener) { | |
+ parent.off(FRAME_ENTRY_LOADED_EVENT, this._frameLoadListener); | |
+ this._frameLoadListener = undefined; | |
+ this._parentFrameRef = undefined; | |
+ } | |
} | |
} |
🤖 Prompt for AI Agents
In packages/core/ui/frame/frame-common.ts around lines 124 to 144, the onLoaded
method adds a once listener to parentFrame without storing references, causing a
potential memory leak if the child unloads first. Fix this by saving parentFrame
and the listener callback as class fields in onLoaded, then override onUnloaded
(around lines 146 to 153) to remove the listener from parentFrame using
parent.off(FRAME_ENTRY_LOADED_EVENT, listener), ensuring proper cleanup of the
event listener.
protected override _notifyFrameEntryLoaded(): void { | ||
const currentEntry = this._currentEntry || this._executingContext?.entry; | ||
|
||
// Note: This is kept as a precaution and must execute before emitting the frame entry event. | ||
// There's a bug with nested frames where sometimes the nested fragment is not recreated at all | ||
// so we manually check on loaded event if the fragment is not recreated and recreate it | ||
if (currentEntry && !currentEntry.fragment) { | ||
this._frameCreateTimeout = setTimeout(() => { | ||
const manager = this._getFragmentManager(); | ||
const transaction = manager.beginTransaction(); | ||
|
||
currentEntry.fragment = this.createFragment(currentEntry, currentEntry.fragmentTag); | ||
_updateTransitions(currentEntry); | ||
transaction.replace(this.containerViewId, currentEntry.fragment, currentEntry.fragmentTag); | ||
transaction.commitAllowingStateLoss(); | ||
|
||
this._frameCreateTimeout = null; | ||
}, 0); | ||
} | ||
|
||
super._notifyFrameEntryLoaded(); | ||
} |
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.
Timeout cleanup is broken & may cause leaks
setTimeout
returns a NodeJS.Timeout
object (on NativeScript’s global typings) – not a number.
typeof this._frameCreateTimeout === 'number'
will therefore be false
, preventing clearTimeout
from running and leaking both the timer and its closure.
Apply the fix below and also clear any pending timeout before scheduling a new one to avoid multiple queued fragment creations:
- if (currentEntry && !currentEntry.fragment) {
- this._frameCreateTimeout = setTimeout(() => {
+ if (currentEntry && !currentEntry.fragment) {
+ if (this._frameCreateTimeout) {
+ clearTimeout(this._frameCreateTimeout as any);
+ }
+ this._frameCreateTimeout = setTimeout(() => {
…
- this._frameCreateTimeout = null;
+ this._frameCreateTimeout = null;
}, 0);
}
and in onUnloaded
:
- if (typeof this._frameCreateTimeout === 'number') {
+ if (this._frameCreateTimeout) {
clearTimeout(this._frameCreateTimeout as any);
this._frameCreateTimeout = null;
}
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/core/ui/frame/index.android.ts around lines 248 to 269, the
_frameCreateTimeout is assigned the result of setTimeout, which is a
NodeJS.Timeout object, not a number. This causes typeof checks against 'number'
to fail and prevents clearTimeout from clearing the timer, leading to leaks. To
fix this, ensure _frameCreateTimeout is typed correctly to accept
NodeJS.Timeout, clear any existing timeout before setting a new one to avoid
multiple queued fragment creations, and call clearTimeout properly in onUnloaded
to clean up the timer.
@coderabbitai review |
✅ Actions performedReview triggered.
|
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.
Actionable comments posted: 0
♻️ Duplicate comments (2)
packages/core/ui/frame/frame-common.ts (2)
125-143
: Critical issue: Event listener memory leak persistsThe event-driven coordination logic is sound, but this implementation still has the memory leak issue identified in previous reviews. The listener registered on
parentFrame
is not properly tracked for cleanup.The solution requires storing both the parent frame reference and the listener function for proper cleanup:
+private _parentFrameRef?: WeakRef<FrameBase>; +private _frameLoadListener?: () => void; @profile public onLoaded() { const parentFrame = this.page?.frame; super.onLoaded(); if (parentFrame?.isLoadingSubviews) { + this._parentFrameRef = new WeakRef(parentFrame); const frameRef = new WeakRef(this); - parentFrame.once(FRAME_ENTRY_LOADED_EVENT, () => { + this._frameLoadListener = () => { const frame = frameRef.deref(); if (frame) { frame.onFrameLoaded(); } - }); + }; + parentFrame.once(FRAME_ENTRY_LOADED_EVENT, this._frameLoadListener); } else { this.onFrameLoaded(); } }
145-156
: Critical issue: Incorrect event listener cleanupThe cleanup logic has two fundamental problems identified in previous reviews:
parentFrame.off(FRAME_ENTRY_LOADED_EVENT)
removes ALL listeners for this event from the parent, not just this frame's listener- The condition
parentFrame.hasListeners(FRAME_ENTRY_LOADED_EVENT)
checks the wrong frameApply this fix to properly clean up the specific listener:
@profile public onUnloaded() { - const parentFrame = this.page?.frame; super.onUnloaded(); - // This is a precaution in case the method is called asynchronously during the loading procedure - if (parentFrame && parentFrame.hasListeners(FRAME_ENTRY_LOADED_EVENT)) { - parentFrame.off(FRAME_ENTRY_LOADED_EVENT); - } + const parent = this._parentFrameRef?.deref(); + if (parent && this._frameLoadListener) { + parent.off(FRAME_ENTRY_LOADED_EVENT, this._frameLoadListener); + this._frameLoadListener = undefined; + this._parentFrameRef = undefined; + } }🧰 Tools
🪛 Biome (1.9.4)
[error] 153-153: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🧹 Nitpick comments (1)
packages/core/ui/frame/frame-common.ts (1)
84-84
: Implement optional chaining for better safetyThe static analysis correctly identifies opportunities to use optional chaining for cleaner and safer code.
Apply these improvements:
-if (parentFrame && parentFrame.canGoBack()) { +if (parentFrame?.canGoBack()) { -if (parentFrame && parentFrame.hasListeners(FRAME_ENTRY_LOADED_EVENT)) { +if (parentFrame?.hasListeners?.(FRAME_ENTRY_LOADED_EVENT)) {Also applies to: 153-153
🧰 Tools
🪛 Biome (1.9.4)
[error] 84-84: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/core/ui/frame/frame-common.ts
(4 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
packages/core/ui/frame/frame-common.ts
[error] 84-84: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 153-153: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: test-android
🔇 Additional comments (5)
packages/core/ui/frame/frame-common.ts (5)
20-21
: LGTM: Well-defined event constantThe introduction of
FRAME_ENTRY_LOADED_EVENT
constant provides a clear, centralized definition for the frame coordination event.
81-81
: Good improvement: Cleaner type handlingRemoving the explicit type casts from
getAncestor
calls improves code readability and suggests better typing has been implemented upstream.Also applies to: 87-87
158-161
: LGTM: Clean separation of concernsThe extraction of frame loading logic into
onFrameLoaded()
with clear responsibilities (process navigation and notify) improves code organization and maintainability.
359-365
: LGTM: Well-implemented event notificationThe
_notifyFrameEntryLoaded
method provides a clean, protected interface for emitting the frame coordination event.
367-367
: Good improvement: Cleaner type handling in frame ancestryRemoving the explicit type casts from
getAncestor
calls in theisNestedWithin
method improves code readability and consistency.Also applies to: 370-370
@coderabbitai review |
✅ Actions performedReview triggered.
|
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.
Actionable comments posted: 0
🧹 Nitpick comments (2)
packages/core/ui/frame/frame-common.ts (2)
125-148
: Excellent implementation of nested frame coordination.The event-driven approach properly addresses the nested frame loading order issue. The use of weak references prevents memory leaks, and storing the callback reference enables proper cleanup.
Consider a minor improvement to make the callback cleanup more robust:
const callback = () => { const frame = frameRef.deref(); if (frame) { frame.onFrameLoaded(); } - - this._frameEntryLoadedCallback = null; }; +this._frameEntryLoadedCallback = callback; parentFrame.once(FRAME_ENTRY_LOADED_EVENT, callback); -this._frameEntryLoadedCallback = callback;This ensures the callback reference is stored before event registration and removes the cleanup from within the callback itself (since
onUnloaded
handles cleanup).
82-95
: Type safety improvement with opportunity for optional chaining.The removal of explicit type casts improves type safety. Consider applying the static analysis suggestion to use optional chaining:
-if (parentFrame && parentFrame.canGoBack()) { +if (parentFrame?.canGoBack()) {This simplifies the null check and improves readability.
🧰 Tools
🪛 Biome (1.9.4)
[error] 85-85: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/core/ui/frame/frame-common.ts
(5 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
packages/core/ui/frame/frame-common.ts
[error] 85-85: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: test-ios
- GitHub Check: test-android
🔇 Additional comments (6)
packages/core/ui/frame/frame-common.ts (6)
20-20
: Good addition for event coordination.The new constant provides a centralized name for the frame entry loaded event, which improves maintainability and reduces the risk of typos in event handling.
48-48
: Essential field for proper event listener cleanup.This field enables storing the callback reference for proper removal from the parent frame, addressing the memory leak concerns from previous reviews.
150-167
: Perfect implementation of event listener cleanup.This method correctly addresses the memory leak concerns from previous reviews by:
- Removing the listener from the parent frame (where it was actually attached)
- Properly nullifying the callback reference
- Following the established pattern for cleanup
169-172
: Good separation of concerns.Extracting the frame loading logic into a dedicated method improves code organization and makes the lifecycle flow more explicit.
370-375
: Clean event emission implementation.The protected method provides a centralized way to emit the frame entry loaded event and allows for platform-specific overrides if needed.
378-388
: Improved type safety with generic ancestor detection.Removing the explicit type casts indicates that
getAncestor
now provides proper generic typing, which improves type safety and reduces boilerplate code.
4987961
to
417c6d6
Compare
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.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/core/ui/frame/frame-common.ts (1)
325-329
: Tiny clean-up: unnecessary cast onthis
this
is already aFrameBase
inside the class; the manual cast is redundant.
(Optional) tighten the declaration and avoid the extra blank line:- let frameAncestor = this as FrameBase; - + let frameAncestor: FrameBase | null = this;Pure readability; no functional difference.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
packages/core/ui/core/view-base/index.ts
(1 hunks)packages/core/ui/frame/callbacks/activity-callbacks.ts
(0 hunks)packages/core/ui/frame/fragment.transitions.android.ts
(7 hunks)packages/core/ui/frame/fragment.transitions.d.ts
(2 hunks)packages/core/ui/frame/frame-common.ts
(2 hunks)packages/core/ui/frame/index.android.ts
(14 hunks)packages/core/ui/frame/index.ios.ts
(2 hunks)
💤 Files with no reviewable changes (1)
- packages/core/ui/frame/callbacks/activity-callbacks.ts
✅ Files skipped from review due to trivial changes (1)
- packages/core/ui/frame/fragment.transitions.d.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/core/ui/core/view-base/index.ts
- packages/core/ui/frame/index.ios.ts
- packages/core/ui/frame/fragment.transitions.android.ts
- packages/core/ui/frame/index.android.ts
🧰 Additional context used
🪛 Biome (1.9.4)
packages/core/ui/frame/frame-common.ts
[error] 82-82: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
let parentFrame = getAncestor(top, 'Frame'); | ||
|
||
while (parentFrame && !parentFrameCanGoBack) { | ||
if (parentFrame && parentFrame.canGoBack()) { | ||
parentFrameCanGoBack = true; | ||
} else { | ||
parentFrame = <FrameBase>getAncestor(parentFrame, 'Frame'); | ||
parentFrame = getAncestor(parentFrame, 'Frame'); | ||
} |
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.
🛠️ Refactor suggestion
Use FrameBase
instead of string literal to get proper type-inference
getAncestor
is now generic and will only infer FrameBase
(and thus expose canGoBack
, goBack
, etc.) when the second argument is the class reference, not the string literal.
Passing 'Frame'
collapses the inferred type to ViewBase | undefined
, forcing the compiler to fall back to any
and silently losing type-safety.
- let parentFrame = getAncestor(top, 'Frame');
+ let parentFrame: FrameBase | undefined = getAncestor(top, FrameBase);
…
- parentFrame = getAncestor(parentFrame, 'Frame');
+ parentFrame = getAncestor(parentFrame, FrameBase);
Besides safer autocompletion, this change will surface misspelled members at compile-time instead of runtime.
(No behavioural impact.)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
let parentFrame = getAncestor(top, 'Frame'); | |
while (parentFrame && !parentFrameCanGoBack) { | |
if (parentFrame && parentFrame.canGoBack()) { | |
parentFrameCanGoBack = true; | |
} else { | |
parentFrame = <FrameBase>getAncestor(parentFrame, 'Frame'); | |
parentFrame = getAncestor(parentFrame, 'Frame'); | |
} | |
// Before: let parentFrame = getAncestor(top, 'Frame'); | |
let parentFrame: FrameBase | undefined = getAncestor(top, FrameBase); | |
while (parentFrame && !parentFrameCanGoBack) { | |
if (parentFrame && parentFrame.canGoBack()) { | |
parentFrameCanGoBack = true; | |
} else { | |
// Before: parentFrame = getAncestor(parentFrame, 'Frame'); | |
parentFrame = getAncestor(parentFrame, FrameBase); | |
} | |
} |
🧰 Tools
🪛 Biome (1.9.4)
[error] 82-82: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🤖 Prompt for AI Agents
In packages/core/ui/frame/frame-common.ts around lines 79 to 86, replace the
string literal 'Frame' passed to getAncestor with the class reference FrameBase
to enable proper type inference. This change ensures getAncestor returns a
FrameBase type, exposing methods like canGoBack with full type safety and better
autocompletion. Update all occurrences of 'Frame' in getAncestor calls within
this block to FrameBase accordingly.
if (this._isReset && !this._attachedToWindow) { | ||
return; | ||
} | ||
|
||
this._frameCreateTimeout = setTimeout(() => { |
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.
@NathanWalker @edusperoni I kept this one in hope of still using it as a precaution but it might no longer be needed. I wonder if there's still a sample we could retry the nested frame bug that the check was intended for.
// there's a bug with nested frames where sometimes the nested fragment is not recreated at all
// so we manually check on loaded event if the fragment is not recreated and recreate it
PR Checklist
What is the current behavior?
Right now, core has inconsistencies with nested Frames when they get destroyed and reused.
This results in errors like the
No view found for id
exception users have been getting lately.The problem is more apparent in flavors like Vue where the entire view tree is being reused after application exit and resume.
What is the new behavior?
This PR ensures attach listener is responsible for creating and attaching entry fragments in the correct order to prevent errors.
Previously, we had a workaround included in #9725 to take care of similar problems but unfortunately the error still persists.
Few things to note:
any
with needed typessubstr
calls withsubstring
Fixes/Implements/Closes #8126.
Summary by CodeRabbit
New Features
Refactor
Bug Fixes