Skip to content

✨ Implement iframe caching mechanism - #5

Closed
QuentinVdr wants to merge 2 commits into
mainfrom
feat/improveIframeItems
Closed

✨ Implement iframe caching mechanism#5
QuentinVdr wants to merge 2 commits into
mainfrom
feat/improveIframeItems

Conversation

@QuentinVdr

@QuentinVdr QuentinVdr commented Oct 8, 2025

Copy link
Copy Markdown
Owner

add IframeCacheProvider and CachedIframePortal components, update ChatItem and StreamItem to utilize caching

Summary by CodeRabbit

  • New Features
    • Persistent iframe caching system that keeps iframe content alive across remounts and moves, including a provider and per-item cache keys.
  • Performance
    • Fewer iframe reloads and reduced visual flicker during layout changes for smoother grid interactions.
  • UX
    • More stable Watch experience with preserved playback and scroll state when rearranging or navigating items.
  • Bug Fixes
    • Improved fullscreen reliability for embedded content.

…hedIframePortal components, update ChatItem and StreamItem to utilize caching
@coderabbitai

coderabbitai Bot commented Oct 8, 2025

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds an iframe caching system (provider, hook, portal) that preserves iframe DOM containers keyed by cacheKey. Integrates caching into GridItem via a cacheKey and container ref; ChatItem/StreamItem supply keys. Wraps the watch route with IframeCacheProvider to enable cache scope.

Changes

Cohort / File(s) Summary
Iframe cache core
src/components/IframeCache/IframeCache.tsx
Adds IframeCacheProvider, useIframeCache, internal maps and hidden container area, TTL-based purge, getIframeContainer and releaseIframeContainer, and CachedIframePortal to mount children into cached containers.
Grid integration
src/components/gridItems/GridItem/GridItem.tsx
Adds cacheKey: string to GridItemProps; creates iframeContainerRef and wraps iframe rendering with CachedIframePortal using the ref and cacheKey.
Item cache keys
src/components/gridItems/ChatItem/ChatItem.tsx, src/components/gridItems/StreamItem/StreamItem.tsx
Compute per-stream cacheKey (chat-${streamName}, stream-${streamName}) and pass it to GridItem; minor JSX reformatting.
Route provider wiring
src/routes/watch.tsx
Wraps the watch layout render tree with IframeCacheProvider; no other rendering or route logic changes.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant WatchRoute as Watch Route
    participant Provider as IframeCacheProvider
    participant GridItem
    participant Portal as CachedIframePortal
    participant Cache as Cache Map
    participant DOM as Hidden Container Area

    User->>WatchRoute: Navigate to /watch
    WatchRoute->>Provider: Render children inside provider
    GridItem->>Portal: Mount with cacheKey + targetRef
    Portal->>Provider: getIframeContainer(cacheKey)
    alt container missing
        Provider->>DOM: create hidden container
        Provider->>Cache: store container by key
    end
    Provider-->>Portal: return container
    Portal->>GridItem: append container into targetRef (mount iframe)
    User->>GridItem: Unmount or move item
    Portal->>Provider: releaseIframeContainer(cacheKey)
    Provider->>DOM: move container back to hidden area (preserve iframe)
    Note over Provider,Cache: TTL-based purge will remove stale containers
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

In burrows of DOM I gently hide,
Keys in paw, no reload ride.
Portals hop and frames stay warm,
Cached and cozy through each reform.
A rabbit guard keeps renders light—🐇💫

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately summarizes the core change of adding an iframe caching mechanism and directly reflects the pull request’s main objective.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/improveIframeItems

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@QuentinVdr
QuentinVdr requested a review from Copilot October 8, 2025 14:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements an iframe caching mechanism to prevent iframes from reloading when grid items are moved, resized, or remounted, improving the user experience by maintaining stream and chat state.

  • Adds IframeCacheProvider context to manage cached iframe containers
  • Introduces CachedIframePortal component to handle iframe portalling and caching
  • Updates ChatItem and StreamItem to use the new caching system with unique cache keys

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/routes/watch.tsx Wraps the Watch component with IframeCacheProvider to enable caching
src/components/gridItems/StreamItem/StreamItem.tsx Adds cache key for stream items and passes it to GridItem
src/components/gridItems/GridItem/GridItem.tsx Integrates CachedIframePortal to wrap iframe with caching functionality
src/components/gridItems/ChatItem/ChatItem.tsx Adds cache key for chat items and passes it to GridItem
src/components/IframeCache/IframeCache.tsx Implements the complete iframe caching system with provider and portal components

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread src/components/gridItems/GridItem/GridItem.tsx
Comment thread src/components/IframeCache/IframeCache.tsx
Comment thread src/components/IframeCache/IframeCache.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
src/components/IframeCache/IframeCache.tsx (1)

11-17: Fast refresh limitation acknowledged.

The static analysis warning about fast refresh is valid but acceptable here. The file exports both the hook (useIframeCache) and components (IframeCacheProvider, CachedIframePortal), which can break fast refresh in development.

If this becomes problematic, consider splitting into separate files:

  • IframeCacheContext.ts for context and hook
  • IframeCacheProvider.tsx for the provider component
  • CachedIframePortal.tsx for the portal component

However, the current structure is reasonable given the tight coupling between these exports.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 30ce78b and 8ed3f40.

📒 Files selected for processing (5)
  • src/components/IframeCache/IframeCache.tsx (1 hunks)
  • src/components/gridItems/ChatItem/ChatItem.tsx (1 hunks)
  • src/components/gridItems/GridItem/GridItem.tsx (4 hunks)
  • src/components/gridItems/StreamItem/StreamItem.tsx (1 hunks)
  • src/routes/watch.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/routes/watch.tsx (1)
src/components/IframeCache/IframeCache.tsx (1)
  • IframeCacheProvider (23-69)
src/components/gridItems/GridItem/GridItem.tsx (2)
src/hooks/useIframeRefresh.ts (1)
  • useIframeRefresh (3-37)
src/components/IframeCache/IframeCache.tsx (1)
  • CachedIframePortal (77-94)
🪛 GitHub Check: Run Linter
src/components/IframeCache/IframeCache.tsx

[warning] 11-11:
Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components

🔇 Additional comments (5)
src/components/gridItems/StreamItem/StreamItem.tsx (1)

16-16: LGTM!

The cache key derivation follows a consistent naming pattern (stream-${streamName}), matching the approach in ChatItem (chat-${streamName}). The multiline formatting improves readability, and the integration with the iframe caching mechanism is correct.

Also applies to: 23-30

src/routes/watch.tsx (1)

59-101: LGTM!

The IframeCacheProvider is correctly placed to wrap all grid items, enabling the iframe caching mechanism throughout the layout. The wrapper preserves all existing rendering logic and props.

src/components/gridItems/ChatItem/ChatItem.tsx (1)

14-14: LGTM!

The cache key derivation is consistent with StreamItem and correctly distinguishes chat iframes from stream iframes using the chat- prefix.

Also applies to: 17-17

src/components/gridItems/GridItem/GridItem.tsx (1)

36-51: Verify the necessity of key={iframeSrc} on the iframe.

The portal integration is well-structured, but the key={iframeSrc} prop on line 40 will cause React to unmount and remount the iframe whenever the iframeSrc changes, even though the cached container persists. This could reduce the effectiveness of the caching mechanism.

For example, in ChatItem, toggling dark theme changes the iframeSrc, which would force a complete iframe remount and reload rather than preserving the iframe state.

Consider whether the key prop is necessary, or if it should be based on a more stable identifier (e.g., key={cacheKey} or removed entirely).

src/components/IframeCache/IframeCache.tsx (1)

77-94: Portal implementation is correct.

The CachedIframePortal correctly:

  • Retrieves the cached container for the given key
  • Moves the container to the target ref on mount (line 84)
  • Releases the container back to the hidden area on unmount (line 89)
  • Creates a portal to render children into the container (line 93)

The appendChild call safely moves the container even if it's already attached elsewhere in the DOM.

Comment thread src/components/IframeCache/IframeCache.tsx
@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2025

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/components/IframeCache/IframeCache.tsx (1)

64-73: Unsafe non-null assertion.

Line 72 uses the non-null assertion operator (!) after checking has(key) and setting the value. While logically safe in the current flow, this pattern was flagged in a previous review and remains unaddressed.

Apply this diff for defensive programming:

   const getIframeContainer = useCallback((key: string): HTMLDivElement => {
     lastUsedRef.current.set(key, Date.now());
     if (!containerMapRef.current.has(key)) {
       const container = document.createElement('div');
       container.style.width = '100%';
       container.style.height = '100%';
       containerMapRef.current.set(key, container);
     }
-    return containerMapRef.current.get(key)!;
+    const container = containerMapRef.current.get(key);
+    if (!container) {
+      throw new Error(`Container for key "${key}" not found in cache.`);
+    }
+    return container;
   }, []);
🧹 Nitpick comments (1)
src/components/IframeCache/IframeCache.tsx (1)

1-17: Fast Refresh limitation with mixed exports.

The linter warns that mixing component exports with hook exports in a single file can break Fast Refresh during development. While not a functional issue, it can degrade the developer experience.

Consider splitting into separate files:

  • src/components/IframeCache/context.ts for the context and hook
  • src/components/IframeCache/IframeCacheProvider.tsx for the provider component
  • src/components/IframeCache/CachedIframePortal.tsx for the portal component
  • src/components/IframeCache/index.ts for re-exports
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ed3f40 and 30e8bf9.

📒 Files selected for processing (1)
  • src/components/IframeCache/IframeCache.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/IframeCache/IframeCache.tsx (3)
src/hooks/useIframeRefresh.ts (5)
  • useIframeRefresh (3-37)
  • node (6-27)
  • timeoutRef (28-34)
  • node (19-24)
  • timeoutRef (29-33)
src/components/gridItems/GridItem/GridItem.tsx (3)
  • ref (20-46)
  • GridItemRef (14-16)
  • () => ({ refreshIframe }) (23-23)
src/components/gridItems/StreamItem/StreamItem.tsx (1)
  • StreamItemProps (9-25)
🪛 GitHub Check: Run Linter
src/components/IframeCache/IframeCache.tsx

[warning] 11-11:
Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components

Comment on lines +41 to +56
const cleanupInterval = setInterval(
() => {
const now = Date.now();
const CACHE_TTL = 10 * 60 * 1000; // 10 minutes

for (const [key, lastUsed] of lastUsedRef.current.entries()) {
if (now - lastUsed > CACHE_TTL) {
const container = containerMapRef.current.get(key);
container?.remove();
containerMapRef.current.delete(key);
lastUsedRef.current.delete(key);
}
}
},
5 * 60 * 1000
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Cleanup may remove actively used containers.

The cleanup interval removes containers based solely on the lastUsed timestamp, without checking whether the container is currently visible (mounted in a target element). If a user keeps a stream open for more than 10 minutes, its container will be removed from the map and DOM while still in use, breaking the UI.

Apply this diff to only evict containers that are in the hidden container:

     const cleanupInterval = setInterval(
       () => {
         const now = Date.now();
         const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
 
         for (const [key, lastUsed] of lastUsedRef.current.entries()) {
           if (now - lastUsed > CACHE_TTL) {
             const container = containerMapRef.current.get(key);
-            container?.remove();
-            containerMapRef.current.delete(key);
-            lastUsedRef.current.delete(key);
+            // Only remove if container is in the hidden area (not actively displayed)
+            if (container && container.parentElement === hiddenContainerRef.current) {
+              container.remove();
+              containerMapRef.current.delete(key);
+              lastUsedRef.current.delete(key);
+            }
           }
         }
       },
       5 * 60 * 1000
     );
📝 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.

Suggested change
const cleanupInterval = setInterval(
() => {
const now = Date.now();
const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
for (const [key, lastUsed] of lastUsedRef.current.entries()) {
if (now - lastUsed > CACHE_TTL) {
const container = containerMapRef.current.get(key);
container?.remove();
containerMapRef.current.delete(key);
lastUsedRef.current.delete(key);
}
}
},
5 * 60 * 1000
);
const cleanupInterval = setInterval(
() => {
const now = Date.now();
const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
for (const [key, lastUsed] of lastUsedRef.current.entries()) {
if (now - lastUsed > CACHE_TTL) {
const container = containerMapRef.current.get(key);
// Only remove if container is in the hidden area (not actively displayed)
if (container && container.parentElement === hiddenContainerRef.current) {
container.remove();
containerMapRef.current.delete(key);
lastUsedRef.current.delete(key);
}
}
}
},
5 * 60 * 1000
);
🤖 Prompt for AI Agents
In src/components/IframeCache/IframeCache.tsx around lines 41 to 56, the cleanup
currently evicts containers purely by lastUsed timestamp which can remove
containers that are actively mounted; modify the eviction to also check that the
container is inside the hidden container before removing: obtain the hidden
container element (hiddenContainerRef.current) and only call container.remove()
and delete from maps when container !== undefined AND hiddenContainerRef.current
exists AND container.parentElement === hiddenContainerRef.current (i.e.,
container is currently in the hidden/offscreen pool); keep the existing TTL and
interval logic unchanged.

Comment on lines +97 to +114
export function CachedIframePortal({ cacheKey, targetRef, children }: CachedIframePortalProps) {
const { getIframeContainer, releaseIframeContainer } = useIframeCache();
const container = getIframeContainer(cacheKey);

useEffect(() => {
// Move container to target when mounted
if (targetRef.current) {
targetRef.current.appendChild(container);
}

return () => {
// Release container when unmounted
releaseIframeContainer(cacheKey);
};
}, [cacheKey, container, targetRef, releaseIframeContainer]);

return createPortal(children, container);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Side effects during render and ref tracking issues.

Multiple issues with the portal implementation:

  1. Line 99: getIframeContainer is called during render, not in an effect. This creates side effects during render (updating lastUsed ref) and violates React's rendering model. It should be moved into the effect.

  2. Effect dependencies: The effect depends on targetRef, but React doesn't track changes to targetRef.current. If the ref's current value changes after mount, the container won't be re-attached to the new target.

  3. Null safety: If targetRef.current is null on mount, the container won't be appended anywhere, leaving the portal in an inconsistent state.

Apply this diff to fix these issues:

 export function CachedIframePortal({ cacheKey, targetRef, children }: CachedIframePortalProps) {
   const { getIframeContainer, releaseIframeContainer } = useIframeCache();
-  const container = getIframeContainer(cacheKey);
+  const containerRef = useRef<HTMLDivElement | null>(null);
 
   useEffect(() => {
+    // Get or create container inside effect
+    const container = getIframeContainer(cacheKey);
+    containerRef.current = container;
+
     // Move container to target when mounted
-    if (targetRef.current) {
+    const target = targetRef.current;
+    if (target) {
-      targetRef.current.appendChild(container);
+      target.appendChild(container);
+    } else {
+      console.warn(`CachedIframePortal: targetRef.current is null for key "${cacheKey}"`);
     }
 
     return () => {
       // Release container when unmounted
       releaseIframeContainer(cacheKey);
     };
   }, [cacheKey, targetRef, getIframeContainer, releaseIframeContainer]);
 
-  return createPortal(children, container);
+  return containerRef.current ? createPortal(children, containerRef.current) : null;
 }
📝 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.

Suggested change
export function CachedIframePortal({ cacheKey, targetRef, children }: CachedIframePortalProps) {
const { getIframeContainer, releaseIframeContainer } = useIframeCache();
const container = getIframeContainer(cacheKey);
useEffect(() => {
// Move container to target when mounted
if (targetRef.current) {
targetRef.current.appendChild(container);
}
return () => {
// Release container when unmounted
releaseIframeContainer(cacheKey);
};
}, [cacheKey, container, targetRef, releaseIframeContainer]);
return createPortal(children, container);
}
export function CachedIframePortal({ cacheKey, targetRef, children }: CachedIframePortalProps) {
const { getIframeContainer, releaseIframeContainer } = useIframeCache();
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
// Get or create container inside effect
const container = getIframeContainer(cacheKey);
containerRef.current = container;
// Move container to target when mounted
const target = targetRef.current;
if (target) {
target.appendChild(container);
} else {
console.warn(`CachedIframePortal: targetRef.current is null for key "${cacheKey}"`);
}
return () => {
// Release container when unmounted
releaseIframeContainer(cacheKey);
};
}, [cacheKey, targetRef, getIframeContainer, releaseIframeContainer]);
return containerRef.current
? createPortal(children, containerRef.current)
: null;
}
🤖 Prompt for AI Agents
In src/components/IframeCache/IframeCache.tsx around lines 97-114, move the call
to getIframeContainer out of render and into a useEffect: create a local state
(or ref) to hold the container and initialize it inside useEffect using
getIframeContainer(cacheKey), then only render the portal when that container
exists. In the same effect (or a separate effect) append the container to
targetRef.current and re-attach if targetRef.current changes by tracking the
actual DOM node (use a ref for previousTarget or a callback ref) so changes to
targetRef.current re-run the attach logic; ensure cleanup releases the container
via releaseIframeContainer(cacheKey) and removes the node from any previous
target to avoid leaks. Also handle the case where targetRef.current is null on
mount by still creating the container and waiting to append it later when
targetRef.current becomes non-null (i.e., perform append on any target change),
keeping createPortal conditional on the container being non-null.

@QuentinVdr QuentinVdr closed this Oct 8, 2025
@QuentinVdr
QuentinVdr deleted the feat/improveIframeItems branch October 8, 2025 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants