Repository navigation
Keep the transcript when a React Activity shows Web Chat again - #6049
Conversation
React 18 and later use their own hook. React 16/17 use a fallback that subscribes in one layout effect, which runs again only when subscribe changes.
useSelector(), <GraphProvider> and useKeyedStoreValue() now use the ponyfill. <GraphProvider> reads the graph and the Redux activities as one snapshot, instead of a store subscription while rendering plus two layout effects. It follows the store through the batched subscription of useSelector(), so React 16/17 re-render it in the same batch as the selectors. Selectors move to module scope, so getSnapshot() stays the same between renders.
createGraphFromStore() subscribed to the store when called and never unsubscribed. <GraphProvider> calls it while rendering, so every mount, and every render that never committed (e.g. the first one of <StrictMode>), left a store subscription behind. The graph now subscribes on its first subscriber, catches up with the store, and unsubscribes after its last one.
…ring <SuggestedActionsComposer> dispatched setRawState() while rendering. With useSyncExternalStore() on React 19, that dispatch notified components with a pending update, and React logged "Cannot update a component while rendering a different component". Dispatch from layout effects instead: the store still has the suggested actions by the time they show.
The page puts React 18 in window.React, and its default variant rendered with the bundled React DOM 16. React DOM 16 has no useSyncExternalStore(). The Fluent variants already render with createRoot() of React DOM 18.
Dictation progress sets the dictate state in Redux and dictateAbortable in a useState() of <Composer>. With useSyncExternalStore() on React 18 and later, the Redux state rendered first, at sync priority, and the microphone button was disabled while dictating until dictateAbortable rendered in the next task. dictateAbortable is now in a keyed store read with useSyncExternalStore(), so both render together.
The mock dispatched its events inside start(), which react-dictate-button calls from an effect. On React 18 and later, "starting" and "dictating" then render in the same frame, unlike with a browser's recognizer, and the forced-colors dictation snapshots differed by a few anti-aliased pixels.
The action sink sets the suggested actions while the store dispatches the incoming activity. With useSyncExternalStore() on React 18 and later, the activity rendered at sync priority and the suggested actions, a useState() of <SuggestedActionsComposer>, one task later. The suggested actions are now in a store read with useSyncExternalStore(), so both render together.
Waiting for one task did not always paint "starting" first: under load, a forced-colors dictation snapshot differed by a few pixels again (1 of about 40 runs).
When the host sets window.React but not window.ReactDOM, the IIFE bundle renders with its React DOM 16, which cannot run React's useSyncExternalStore(). Hide it from window.React there, so the ponyfill falls back as with React 16/17, like before. typingIndicator.scroll.html covers this setup again.
…yed store may notify
…ribers, as when an Activity shows Web Chat again
The graph follows the store only while it has subscribers. A hidden React 19.2 <Activity> runs the cleanup of effects, so the graph stops following the store; showing Web Chat again with a new element renders before the effects subscribe again, and read a graph that missed the changes made while hidden. The development check of useActivities() threw. getState() now catches up with the store when the graph has no subscribers. It returns the same state until the store activities change.
…tore' into OEvgeny/fix-activity-graph-catch-up
There was a problem hiding this comment.
🟡 Changes recommended
After an unsubscribed store update, calling graph.act() can throw a nested-transaction error and prematurely advance the activity cursor, causing later reads to skip pending updates.
1 open issue
What changed in this PR
This follow-up to #6040 refreshes Web Chat’s graph after store changes made while React <Activity> hides it, preventing stale transcript reads when it becomes visible again.
Changes:
- Catch up on graph reads without keeping unsubscribed graphs connected to the store.
- Add graph-level and browser regression coverage for hide/show behavior with new and reused elements.
- Document the development-build fix in the changelog.
| File | Description |
|---|---|
packages/core/src/graph/createGraphFromStore.ts |
Adds catch-up reads and advances activity tracking before transactions. |
packages/core/src/graph/createGraphFromStore.spec.ts |
Checks unsubscribed catch-up and stable repeated snapshots. |
.changelog/6049-fix-activity-graph-catch-up.md |
Records the React Activity fix. |
__tests__/html2/react19/activity.hideShow.transcript.sameElement.html |
Checks transcript preservation when reusing the same element. |
__tests__/html2/react19/activity.hideShow.transcript.html |
Checks transcript preservation when showing a new element. |
Review effort: Balanced
| // Catches up with changes made while the graph has no subscribers, e.g. when a hidden <Activity> (React 19.2+) ran the | ||
| // cleanup of effects and renders before subscribing again. Returns the same state until the store activities change. | ||
| graph.getState = () => { | ||
| unsubscribeFromStore || handleStoreChange(); |
There was a problem hiding this comment.
Confirmed: the new spec scenario (unsubscribed → dispatch → graph.act() → getState()) threw "Another transaction is ongoing" and then lost the activity. Fixed in 7edbda4: act() now catches up before the caller's transaction, reads inside a transaction no longer catch up, and prevActivities advances only after the catch-up applied. Web Chat does not reach this today: createGraphFromStore is the only caller of graph.act() and the graph is not exposed to hosts.
Reads inside a transaction no longer catch up with the store, so act() on a graph without subscribers does not throw "Another transaction is ongoing" and lose the activities. prevActivities advances only after the catch-up applied.

Changelog Entry
Fixed
<Activity>shows Web Chat again, in PR #6049, by @OEvgenyDescription
A hidden React 19.2
<Activity>runs effect cleanups, so the graph has no subscribers and stops following the store. Showing Web Chat with a new element renders before effects subscribe again, and that render read a stale graph.Design
prevActivitiesis set beforeact(), whose middleware readsgetState(): anact()that throws is no longer retried (and rethrown) on every later store change.Specific Changes
createGraphFromStore.ts: catch up ingetState()without subscriberscreateGraphFromStore.spec.ts: catch-up scenario, plus a read without subscribersreact19/activity.hideShow.transcript.html: hide, add a message to the store, show a new element: both messagesreact19/activity.hideShow.transcript.sameElement.html: same, showing the same elementI have added tests and executed them locally
I have added
.changelog/<PR#>-<slug>.mdI have updated documentation
Review Checklist
z-index)package.jsonandpackage-lock.jsonreviewed