From 616f762cd2efcef0bb1081aa5342748753784f10 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Thu, 22 Oct 2026 04:44:25 -0700 Subject: [PATCH 1/5] Fix state updates while rendering in the live region and read/acknowledged state useLiveRegion and the typing indicator queued live region nodes in useMemo, the "New messages" button marked activities as read and the transcript marked them as acknowledged in useMemo. React development builds log "Cannot update a component while rendering a different component" for each. Move the updates into layout effects (still before paint) and queue a live region node once per deps change, also in Strict Mode. --- ...ollToEndButton.noUpdateWhileRendering.html | 95 ++++++++++++++++++ ...ton.noUpdateWhileRendering.html.snap-1.png | Bin 0 -> 30607 bytes .../html2/toast/noUpdateWhileRendering.html | 89 ++++++++++++++++ .../noUpdateWhileRendering.html.snap-1.png | Bin 0 -> 6727 bytes ...tor.liveRegion.noUpdateWhileRendering.html | 79 +++++++++++++++ ...ion.noUpdateWhileRendering.html.snap-1.png | Bin 0 -> 4656 bytes packages/component/src/BasicTranscript.tsx | 14 +-- .../ChatHistory/private/ScrollToEndButton.tsx | 22 ++-- .../src/Transcript/LiveRegionTranscript.tsx | 4 +- .../private/useStateWithOptimisticRef.ts | 13 --- .../providers/LiveRegionTwin/useLiveRegion.ts | 19 +++- 11 files changed, 296 insertions(+), 39 deletions(-) create mode 100644 __tests__/html2/scrollToEndButton/scrollToEndButton.noUpdateWhileRendering.html create mode 100644 __tests__/html2/scrollToEndButton/scrollToEndButton.noUpdateWhileRendering.html.snap-1.png create mode 100644 __tests__/html2/toast/noUpdateWhileRendering.html create mode 100644 __tests__/html2/toast/noUpdateWhileRendering.html.snap-1.png create mode 100644 __tests__/html2/typing/typingIndicator.liveRegion.noUpdateWhileRendering.html create mode 100644 __tests__/html2/typing/typingIndicator.liveRegion.noUpdateWhileRendering.html.snap-1.png delete mode 100644 packages/component/src/providers/ActivityLogicalGrouping/private/useStateWithOptimisticRef.ts diff --git a/__tests__/html2/scrollToEndButton/scrollToEndButton.noUpdateWhileRendering.html b/__tests__/html2/scrollToEndButton/scrollToEndButton.noUpdateWhileRendering.html new file mode 100644 index 0000000000..efdb736b8a --- /dev/null +++ b/__tests__/html2/scrollToEndButton/scrollToEndButton.noUpdateWhileRendering.html @@ -0,0 +1,95 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/scrollToEndButton/scrollToEndButton.noUpdateWhileRendering.html.snap-1.png b/__tests__/html2/scrollToEndButton/scrollToEndButton.noUpdateWhileRendering.html.snap-1.png new file mode 100644 index 0000000000000000000000000000000000000000..e531ed934651d824b28a8dc5f50743fcbfe6a1e1 GIT binary patch literal 30607 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 + + + + + + + + +
+ + + + diff --git a/__tests__/html2/toast/noUpdateWhileRendering.html.snap-1.png b/__tests__/html2/toast/noUpdateWhileRendering.html.snap-1.png new file mode 100644 index 0000000000000000000000000000000000000000..e142cff088fc0e3d6e4f56d37e9f4cf3411fd4fa GIT binary patch literal 6727 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 QRs?SRVtc*r+HZgU2j;r1!~g&Q literal 0 HcmV?d00001 diff --git a/__tests__/html2/typing/typingIndicator.liveRegion.noUpdateWhileRendering.html b/__tests__/html2/typing/typingIndicator.liveRegion.noUpdateWhileRendering.html new file mode 100644 index 0000000000..f88329898d --- /dev/null +++ b/__tests__/html2/typing/typingIndicator.liveRegion.noUpdateWhileRendering.html @@ -0,0 +1,79 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/typing/typingIndicator.liveRegion.noUpdateWhileRendering.html.snap-1.png b/__tests__/html2/typing/typingIndicator.liveRegion.noUpdateWhileRendering.html.snap-1.png new file mode 100644 index 0000000000000000000000000000000000000000..fb78f33e6993e98d009c85d7a8f9bb69620b14c9 GIT binary patch literal 4656 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 KEQ?Nh-1;Y>=@A0} literal 0 HcmV?d00001 diff --git a/packages/component/src/BasicTranscript.tsx b/packages/component/src/BasicTranscript.tsx index a80949ff6e..763cc6ab73 100644 --- a/packages/component/src/BasicTranscript.tsx +++ b/packages/component/src/BasicTranscript.tsx @@ -8,6 +8,7 @@ import React, { Fragment, memo, useCallback, + useLayoutEffect, useMemo, useRef, type KeyboardEventHandler, @@ -493,15 +494,10 @@ const InternalTranscriptScrollable = ({ children, onFocusFiller }: InternalTrans const stickyChangedToTrue = prevSticky !== sticky && sticky; - useMemo( - () => - stickyChangedToTrue && - // TODO: [P2] Both `markActivityKeyAsRead` and `markAllAsAcknowledged` hook are setters of useState. - // This means, in a render loop, we will be calling setter and will cause another re-render. - // This is not trivial but we should think if there is a way to avoid this. - markAllAsAcknowledged(), - [markAllAsAcknowledged, stickyChangedToTrue] - ); + // `markAllAsAcknowledged` is a setter of , it cannot be called while rendering this component. + useLayoutEffect(() => { + stickyChangedToTrue && markAllAsAcknowledged(); + }, [markAllAsAcknowledged, stickyChangedToTrue]); // We need to check if `children` is `false` or not. // If `children` is `false`, React.Children.count(children) will still return 1 (truthy). diff --git a/packages/component/src/ChatHistory/private/ScrollToEndButton.tsx b/packages/component/src/ChatHistory/private/ScrollToEndButton.tsx index 0bf0f06dac..fdaebb7e10 100644 --- a/packages/component/src/ChatHistory/private/ScrollToEndButton.tsx +++ b/packages/component/src/ChatHistory/private/ScrollToEndButton.tsx @@ -1,5 +1,5 @@ import { hooks } from 'botframework-webchat-api'; -import React, { Fragment, memo, MutableRefObject, useCallback, useMemo } from 'react'; +import React, { Fragment, memo, MutableRefObject, useCallback, useLayoutEffect, useMemo } from 'react'; import { useAnimatingToEnd, useAtEnd, useScrollToEnd, useSticky } from 'react-scroll-to-bottom'; import { useRefFrom } from 'use-ref-from'; @@ -46,17 +46,17 @@ const useScrollToEndRenderResult = (terminatorRef: MutableRefObject { - // This code need to be careful reviewed as it will cause another render. The code should be converging. - // After we call `markActivityKeyAsRead`, everything will be read and nothing will be unread. - // That means, in next render, `unreadActivityKeys` will be emptied and the `markActivityKeyAsRead` will not get called again. - if (sticky && unreadActivityKeys.length) { - markActivityKeyAsRead(unreadActivityKeys[unreadActivityKeys.length - 1]); - - return []; - } + const nextUnreadActivityKeys = useMemo( + () => (sticky && unreadActivityKeys.length ? [] : unreadActivityKeys), + [sticky, unreadActivityKeys] + ); - return unreadActivityKeys; + // This code need to be careful reviewed as it will cause another render. The code should be converging. + // After we call `markActivityKeyAsRead`, everything will be read and nothing will be unread. + // That means, in next render, `unreadActivityKeys` will be emptied and the `markActivityKeyAsRead` will not get called again. + // Layout effect: updating while rendering this component is not allowed. + useLayoutEffect(() => { + sticky && unreadActivityKeys.length && markActivityKeyAsRead(unreadActivityKeys[unreadActivityKeys.length - 1]); }, [markActivityKeyAsRead, sticky, unreadActivityKeys]); const nextUnreadActivityKeysRef = useRefFrom(nextUnreadActivityKeys); diff --git a/packages/component/src/Transcript/LiveRegionTranscript.tsx b/packages/component/src/Transcript/LiveRegionTranscript.tsx index a56adb2524..8baf10d7e2 100644 --- a/packages/component/src/Transcript/LiveRegionTranscript.tsx +++ b/packages/component/src/Transcript/LiveRegionTranscript.tsx @@ -7,7 +7,7 @@ import LiveRegionActivity from '../LiveRegion/LiveRegionActivity'; import tabbableElements from '../Utils/tabbableElements'; import useLocalizeAccessKey from '../hooks/internal/useLocalizeAccessKey'; import useSuggestedActionsAccessKey from '../hooks/internal/useSuggestedActionsAccessKey'; -import { useQueueStaticElement } from '../providers/LiveRegionTwin'; +import { useLiveRegion, useQueueStaticElement } from '../providers/LiveRegionTwin'; import LiveRegionSendFailed from './LiveRegion/SendFailed'; import isPresentational from './LiveRegion/isPresentational'; import useTypistNames from './useTypistNames'; @@ -128,7 +128,7 @@ const LiveRegionTranscript = ({ activityElementMapRef }: LiveRegionTranscriptPro keyedActivities ]); - useMemo(() => typingIndicator && queueStaticElement(typingIndicator), [queueStaticElement, typingIndicator]); + useLiveRegion(() => typingIndicator, [typingIndicator]); return ; }; diff --git a/packages/component/src/providers/ActivityLogicalGrouping/private/useStateWithOptimisticRef.ts b/packages/component/src/providers/ActivityLogicalGrouping/private/useStateWithOptimisticRef.ts deleted file mode 100644 index 84dc83bb98..0000000000 --- a/packages/component/src/providers/ActivityLogicalGrouping/private/useStateWithOptimisticRef.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useCallback, useRef, useState } from 'react'; - -export default function useStateWithOptimisticRef(initialState: T | (() => T)) { - const [state, setState] = useState(initialState); - const stateRef = useRef(state); - - const setStateWithRefUpdate = useCallback((newState: T) => { - setState(newState); - stateRef.current = newState; - }, []); - - return [state, setStateWithRefUpdate, stateRef] as const; -} diff --git a/packages/component/src/providers/LiveRegionTwin/useLiveRegion.ts b/packages/component/src/providers/LiveRegionTwin/useLiveRegion.ts index 7e9c70a300..739ac05077 100644 --- a/packages/component/src/providers/LiveRegionTwin/useLiveRegion.ts +++ b/packages/component/src/providers/LiveRegionTwin/useLiveRegion.ts @@ -1,6 +1,7 @@ -import { useMemo, type DependencyList, type ReactElement } from 'react'; +import { useLayoutEffect, useRef, type DependencyList, type ReactElement } from 'react'; import { useRefFrom } from 'use-ref-from'; +import shallowEquals from '../../Utils/shallowEquals'; import useQueueStaticElement from './useQueueStaticElement'; /** @@ -14,14 +15,24 @@ import useQueueStaticElement from './useQueueStaticElement'; export default function useLiveRegion( createNode: () => false | null | ReactElement | string | undefined, deps: DependencyList -) { +): void { const createNodeRef = useRefFrom(createNode); + const queuedDepsRef = useRef(); const queueStaticElement = useQueueStaticElement(); - useMemo(() => { + // Queue after render, not during render: updating while rendering another component is not allowed. + // Layout effect, so the node is in the live region before the next paint, as when it was queued during render. + useLayoutEffect(() => { + // Strict Mode runs the effect twice on mount with the same deps, the node should only be queued once. + if (queuedDepsRef.current && shallowEquals(queuedDepsRef.current, deps)) { + return; + } + + queuedDepsRef.current = deps; + const node = createNodeRef?.current(); node && queueStaticElement(node); - // eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/use-memo + // eslint-disable-next-line react-hooks/exhaustive-deps }, [...deps, createNodeRef, queueStaticElement]); } From e63c5229e984b24c75d693afc7f184ea04f33fb6 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Thu, 22 Oct 2026 04:44:25 -0700 Subject: [PATCH 2/5] Keep the activity-to-group map of part grouping in a ref Part grouping registers itself while rendering and its activities read their group while rendering. The map was also kept in a state that was never read, so the registration updated while rendering. --- .../ActivityLogicalGroupingComposer.ts | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/component/src/providers/ActivityLogicalGrouping/ActivityLogicalGroupingComposer.ts b/packages/component/src/providers/ActivityLogicalGrouping/ActivityLogicalGroupingComposer.ts index d2e402724b..2fdb1b78c8 100644 --- a/packages/component/src/providers/ActivityLogicalGrouping/ActivityLogicalGroupingComposer.ts +++ b/packages/component/src/providers/ActivityLogicalGrouping/ActivityLogicalGroupingComposer.ts @@ -7,7 +7,6 @@ import ActivityLogicalGroupingContext, { type GroupState, type LogicalGrouping } from './private/ActivityLogicalGroupingContext'; -import useStateWithOptimisticRef from './private/useStateWithOptimisticRef'; const activityLogicalGroupingComposerPropsSchema = pipe( object({ @@ -22,9 +21,8 @@ const ActivityLogicalGroupingComposer = (props: ActivityLogicalGroupingComposerP const { children } = validateProps(activityLogicalGroupingComposerPropsSchema, props); const logicalGroupingsRef = useRef>(new Map()); - const [_activityToGroupMap, setActivityToGroupMap, activityToGroupMapRef] = useStateWithOptimisticRef< - ReadonlyMap - >(new Map()); + // A ref, not a state: part grouping adds itself while rendering, and its activities read their group while rendering. + const activityToGroupMapRef = useRef>(new Map()); const addLogicalGrouping = useCallback( (grouping: LogicalGrouping) => { @@ -35,24 +33,22 @@ const ActivityLogicalGroupingComposer = (props: ActivityLogicalGroupingComposerP const toRemoveSet = new Set(prevGroupingKeys).difference(new Set(grouping.activityKeys)); const toAddSet = new Set(grouping.activityKeys).difference(new Set(prevGroupingKeys)); - setActivityToGroupMap( - new Map( - [].concat( - Array.from(activityToGroupMapRef.current).filter(([, value]) => !!value && !toRemoveSet.has(value)), - Array.from(toAddSet).map(activityKey => [activityKey, grouping.key] as const) - ) + activityToGroupMapRef.current = new Map( + [].concat( + Array.from(activityToGroupMapRef.current).filter(([, value]) => !!value && !toRemoveSet.has(value)), + Array.from(toAddSet).map(activityKey => [activityKey, grouping.key] as const) ) ); }, - [activityToGroupMapRef, setActivityToGroupMap] + [activityToGroupMapRef] ); const removeLogicalGrouping = useCallback( (key: string) => { logicalGroupingsRef.current.delete(key); - setActivityToGroupMap(new Map([...activityToGroupMapRef.current].filter(([, value]) => value !== key))); + activityToGroupMapRef.current = new Map([...activityToGroupMapRef.current].filter(([, value]) => value !== key)); }, - [activityToGroupMapRef, setActivityToGroupMap] + [activityToGroupMapRef] ); const getLogicalGroupKey = useCallback( From d20a9f65b9b71a4bb446758c058e8b1c3214f250 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Thu, 22 Oct 2026 04:44:25 -0700 Subject: [PATCH 3/5] Stop ignoring "Cannot update a component while rendering a different component" in tests --- .../partGroupingShouldUseMaxTimestamp.html | 2 -- __tests__/html2/themeProvider/override.html | 18 ++++++------------ .../test/harness/src/host/common/host/done.js | 5 +---- .../src/globals/testHelpers/hideKnownError.js | 5 +---- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/__tests__/html2/activityOrdering/partGroupingShouldUseMaxTimestamp.html b/__tests__/html2/activityOrdering/partGroupingShouldUseMaxTimestamp.html index 04b302d31e..adefcf09cb 100644 --- a/__tests__/html2/activityOrdering/partGroupingShouldUseMaxTimestamp.html +++ b/__tests__/html2/activityOrdering/partGroupingShouldUseMaxTimestamp.html @@ -24,10 +24,8 @@ import { version } from 'react'; const IGNORING_KEYWORDS = [ - 'Cannot update a component', 'Support for defaultProps', 'ReactDOM.render is no longer supported', - 'To locate the bad setState()', 'has been deprecated, use `import {' ]; diff --git a/__tests__/html2/themeProvider/override.html b/__tests__/html2/themeProvider/override.html index ca18774bb9..1e69e12434 100644 --- a/__tests__/html2/themeProvider/override.html +++ b/__tests__/html2/themeProvider/override.html @@ -54,10 +54,7 @@ !( message.includes( 'Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.' - ) || - message.includes('ReactDOM.render is no longer supported in React 18. Use createRoot instead.') || - // TODO: [P0] We should fix the "Cannot update a component while rendering a different component" error. - (message.includes('Cannot update a component') && message.includes('while rendering a different component')) + ) || message.includes('ReactDOM.render is no longer supported in React 18. Use createRoot instead.') ) ) { consoleError(...args); @@ -108,14 +105,11 @@ stylesRoot, variant: 'fluent' }, - createElement( - ReactWebChat, - { - className: 'webchat-with-overrides', - directLine, - store - } - ) + createElement(ReactWebChat, { + className: 'webchat-with-overrides', + directLine, + store + }) ) ) ) diff --git a/packages/test/harness/src/host/common/host/done.js b/packages/test/harness/src/host/common/host/done.js index 0d2e81e460..776d1451cb 100644 --- a/packages/test/harness/src/host/common/host/done.js +++ b/packages/test/harness/src/host/common/host/done.js @@ -25,10 +25,7 @@ module.exports = (webDriver, resolve) => !( message.includes( 'Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.' - ) || - message.includes('ReactDOM.render is no longer supported in React 18. Use createRoot instead.') || - // TODO: [P0] We should fix the "Cannot update a component while rendering a different component" error. - (message.includes('Cannot update a component') && message.includes('while rendering a different component')) + ) || message.includes('ReactDOM.render is no longer supported in React 18. Use createRoot instead.') ) ); diff --git a/packages/test/page-object/src/globals/testHelpers/hideKnownError.js b/packages/test/page-object/src/globals/testHelpers/hideKnownError.js index 608f3c5e62..9d9f89eb2b 100644 --- a/packages/test/page-object/src/globals/testHelpers/hideKnownError.js +++ b/packages/test/page-object/src/globals/testHelpers/hideKnownError.js @@ -9,10 +9,7 @@ export default function hideKnownError() { !( message.includes( 'Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.' - ) || - message.includes('ReactDOM.render is no longer supported in React 18. Use createRoot instead.') || - // TODO: [P0] We should fix the "Cannot update a component while rendering a different component" error. - (message.includes('Cannot update a component') && message.includes('while rendering a different component')) + ) || message.includes('ReactDOM.render is no longer supported in React 18. Use createRoot instead.') ) ) { consoleError(...args); From 1121f33e31366afa61acfd9e6c1f012143d69fb7 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Thu, 22 Oct 2026 04:48:16 -0700 Subject: [PATCH 4/5] Add the changelog fragment --- .changelog/5958-fix-render-phase-updates.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/5958-fix-render-phase-updates.md diff --git a/.changelog/5958-fix-render-phase-updates.md b/.changelog/5958-fix-render-phase-updates.md new file mode 100644 index 0000000000..fa106d3dc6 --- /dev/null +++ b/.changelog/5958-fix-render-phase-updates.md @@ -0,0 +1,3 @@ +### Fixed + +- Fixed state updates while rendering that React development builds logged as "Cannot update a component while rendering a different component", from the toaster, the typing indicator, the "New messages" button, and part grouping, in PR [#5958](https://github.com/microsoft/BotFramework-WebChat/pull/5958), by [@OEvgeny](https://github.com/OEvgeny) From c4d28ede992aa009eb8fe22de85aff2667a6ba54 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Thu, 22 Oct 2026 06:18:36 -0700 Subject: [PATCH 5/5] Sync activities already in the store into the graph before other layout effects re-render A rehydrated store has activities before the graph is created, but the graph only added them on the next dispatch, and synchronized in a passive effect. Queuing the rehydrated notification to the live region in a layout effect re-renders the transcript (useActivities) after passive effects but before the graph state update, and the development assertion found 0 activities in the graph and 1 in Redux. - createGraphFromStore: add the store's activities when the graph is created - GraphProvider: subscribe and synchronize in a layout effect, so the graph state is updated in the same render as other layout effect updates --- packages/api-graph/src/private/GraphProvider.tsx | 6 ++++-- packages/core/src/graph/createGraphFromStore.ts | 9 +++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/api-graph/src/private/GraphProvider.tsx b/packages/api-graph/src/private/GraphProvider.tsx index 7872eaf540..116567b015 100644 --- a/packages/api-graph/src/private/GraphProvider.tsx +++ b/packages/api-graph/src/private/GraphProvider.tsx @@ -7,7 +7,7 @@ import { type GraphSubscriber, type Identifier } from 'botframework-webchat-core/graph.js'; -import React, { memo, useEffect, useMemo, useState } from 'react'; +import React, { memo, useLayoutEffect, useMemo, useState } from 'react'; import { custom, function_, is, object, optional, parse, pipe, readonly, type InferInput } from 'valibot'; import GraphContext, { graphContextSchema, GraphContextType } from './GraphContext'; @@ -33,7 +33,9 @@ function GraphProvider(props: GraphProviderProps) { const [orderedActivityNodes, setOrderedActivityNodes] = useState(EMPTY_ARRAY); - useEffect(() => { + // Layout effect: activities already in the store (e.g. rehydrated) must be in this state before the re-render of any other layout effect. + // Otherwise, that re-render would see activities in Redux which are not in the graph yet. + useLayoutEffect(() => { // Sync between graph and `orderedActivities`. const handleChange: GraphSubscriber = record => { const state = graph.getState(); diff --git a/packages/core/src/graph/createGraphFromStore.ts b/packages/core/src/graph/createGraphFromStore.ts index 8436473028..f1278b4753 100644 --- a/packages/core/src/graph/createGraphFromStore.ts +++ b/packages/core/src/graph/createGraphFromStore.ts @@ -33,7 +33,7 @@ function createGraphFromStore(store: ReturnType): SlantGraph graph.act(graph => graph.upsert(othersAudience, channelAudience, selfAudience)); - store.subscribe(() => { + const handleStoreChange = () => { const { activities }: { activities: readonly Activity[] } = store.getState(); if (Object.is(activities, prevActivities)) { @@ -113,7 +113,12 @@ function createGraphFromStore(store: ReturnType): SlantGraph }); prevActivities = activities; - }); + }; + + store.subscribe(handleStoreChange); + + // Activities already in the store (e.g. rehydrated) are added now, not on the next dispatch. + handleStoreChange(); return graph; }