redux-toolkit icon indicating copy to clipboard operation
redux-toolkit copied to clipboard

RTK Query subscriptions do not update in React portals rendered into a new window

Open HMemmi opened this issue 3 months ago • 1 comments

What I’m doing

I’m building a feature where the user can “pop out” a panel into a new browser window. I create the new window and render React content into it via createPortal:

const [container, setContainer] = useState(null);
const [panelWindow, setPanelWindow] = useState(null);

useEffect(() => {
  const popup = window.open("", "_blank", "width=600,height=800");
  if (!popup) return;

  const el = popup.document.createElement("div");
  popup.document.body.appendChild(el);

  setPanelWindow(popup);
  setContainer(el);

  return () => popup.close();
}, []);

if (!container || !panelWindow) return null;

//wrapped by the same Redux <Provider>
return createPortal(
  <Panel />,
  container
);

The Panel contains RTK Query hooks (useGetXQuery, useMutation, etc.) that subscribe to data.

What works • If I use vanilla Redux slices (dispatch / useSelector), the panel updates correctly in the new window. • If I use axios + useState, it also works fine. • RTK Query mutations (usePatchSomethingMutation) dispatch correctly and update the cache. • Redux DevTools shows the cache entry as updated and fulfilled.

What breaks • Components rendered into the new window with RTK Query hooks (useGetXQuery) do not re-render when the cache updates. • debugValue inside the hook shows the updated data (e.g. 39 rows), but data / currentData in the component remain stale (e.g. 38 rows). • If I force any re-render (resizing, dispatching a dummy action, or making the main window visible by even a single pixel), the panel updates immediately with the correct data. • This only happens when the new window is maximized; if the main window is visible even slightly or the popup is resized, the panel updates immediately.

Why I think this is happening • RTK Query hooks rely on useSyncExternalStore subscriptions. • useSyncExternalStore seems tied to the main document visibility/focus for scheduling updates. • Since the component tree is portaled into a different window.document, subscriptions don’t flush updates until the main document becomes visible again. • Plain Redux works because reducers just update state immediately, without visibility checks.

Steps to reproduce

  1. Render an RTK Query-enabled component into a new window using createPortal.
  2. Maximize the new window.
  3. Trigger a mutation that updates the cached query data.
  4. Observe: DevTools shows the cache updated, but the new window UI does not.
  5. Force a re-render (resize window, dispatch dummy action, show main document). Now the new window updates.

Expected behavior RTK Query subscriptions should notify and re-render components in any window as long as they are part of the same React tree + Redux store. Visibility of the original document should not matter.

Workarounds tried • refetch() → does not help. • Dispatching a dummy action → works, but hacky. • Separate React root in popup with <Provider store={store}> → still broken for RTK Query (but plain Redux works fine).

Environment • React 19.1.0 • Redux Toolkit 2.9.0 • Browser: Chrome (latest), also reproduced in Firefox

Here is a minimal reproducible example: https://codesandbox.io/p/sandbox/zpqhcz

HMemmi avatar Sep 19 '25 10:09 HMemmi

It might not be useSyncExternalStore per se, but rather the autoBatch enhancer, which defaults to using requestAnimationFrame as the timing mechanism to trigger some updates:

  • https://redux-toolkit.js.org/api/autoBatchEnhancer

What happens if you override your configureStore setup to customize this with one of the other timing options?

markerikson avatar Sep 20 '25 02:09 markerikson