Skip to content

Commit ae1daf1

Browse files
committed
fix: properly track view tree reordering changes
- Add renderSequenceKey to detect when child order changes - Implement commitRenderSequence to compare sequences after render - Use useLayoutEffect to safely update state after render completes - Only increment version when sequence actually differs - Prevents infinite re-render loops and unnecessary native updates
1 parent e38ce42 commit ae1daf1

File tree

2 files changed

+49
-6
lines changed

2 files changed

+49
-6
lines changed

src/components/SwiftUI.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PropsWithChildren, ReactElement, ReactNode, useCallback, useEffect, useId } from "react";
1+
import { PropsWithChildren, ReactElement, ReactNode, useCallback, useEffect, useId, useLayoutEffect } from "react";
22
import { StyleProp, ViewStyle } from "react-native";
33
import { SwiftUIParentIdProvider } from "./../contexts";
44
import { SwiftUIProvider, useSwiftUIContext } from "./../contexts/SwiftUIContext";
@@ -46,7 +46,15 @@ export const SwiftUIRoot = ({
4646
onEvent: rootOnEvent,
4747
debug = false,
4848
}: PropsWithChildren<SwiftUIProps>): ReactNode => {
49-
const { nativeRef, getEventHandler, nodesKey, getNodes, renderSequence } = useSwiftUIContext();
49+
const {
50+
nativeRef,
51+
getEventHandler,
52+
nodesKey,
53+
renderSequenceKey,
54+
getNodes,
55+
renderSequence,
56+
commitRenderSequence,
57+
} = useSwiftUIContext();
5058

5159
const log = useCallback(
5260
(message: string, ...args: unknown[]) => {
@@ -59,13 +67,22 @@ export const SwiftUIRoot = ({
5967

6068
const nodes = getNodes();
6169
log(`rendering with ${nodes.size} nodes`);
62-
renderSequence.current = []; // Reset render sequence
70+
71+
// Reset sequence before children render so it's rebuilt fresh each time
72+
renderSequence.current = [];
73+
74+
// After render, commit the sequence - use layout effect to run synchronously
75+
useLayoutEffect(() => {
76+
commitRenderSequence();
77+
});
78+
79+
// Rebuild view tree when nodes or order changes
6380
useEffect(() => {
6481
const viewTree = buildViewTree(nodes, renderSequence.current);
6582
log(`updating view tree`, viewTree);
6683
nativeRef.current?.setNativeProps({ viewTree: JSON.stringify(viewTree) });
6784
// eslint-disable-next-line react-hooks/exhaustive-deps
68-
}, [nativeRef, nodesKey]);
85+
}, [nativeRef, nodesKey, renderSequenceKey]);
6986

7087
const handleEvent: SwiftUIProps["onEvent"] = (event) => {
7188
const { id, name, value } = event.nativeEvent;

src/contexts/SwiftUIContext.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ export type NodeRegistry = Map<string, { node: ViewTreeNode; parentId?: string }
2323
export type SwiftUIContextValue = {
2424
getEventHandler: (id: string, name: string) => EventHandler | undefined;
2525
nodesKey: string;
26+
renderSequenceKey: string;
2627
getNodes: () => NodeRegistry;
2728
nativeRef: RefObject<React.ComponentRef<typeof SwiftUIRootNativeComponent> | null>;
2829
recordRenderOrder: (id: string) => void;
30+
commitRenderSequence: () => void;
2931
registerEvents: (id: string, events: Record<string, EventHandler | undefined>) => void;
3032
registerEvent: (id: string, name: string, handler: EventHandler) => void;
3133
registerNode: (node: ViewTreeNode, parentId: string) => void;
@@ -49,7 +51,9 @@ export const SwiftUIProvider: FunctionComponent<PropsWithChildren<SwiftUIProvide
4951
const eventRegistry = useRef<EventRegistry>(new Map());
5052
const nodeRegistry = useRef<NodeRegistry>(new Map());
5153
const [nodeRegistryVersion, setNodeRegistryVersion] = useState(0);
54+
const [renderSequenceVersion, setRenderSequenceVersion] = useState(0);
5255
const renderSequence = useRef<string[]>([]);
56+
const previousRenderSequence = useRef<string[]>([]);
5357
const nativeRef = useRef<React.ComponentRef<typeof SwiftUIRootNativeComponent> | null>(null);
5458

5559
const log = useCallback(
@@ -67,6 +71,11 @@ export const SwiftUIProvider: FunctionComponent<PropsWithChildren<SwiftUIProvide
6771
// eslint-disable-next-line react-hooks/exhaustive-deps
6872
}, [nodeRegistryVersion]);
6973

74+
const renderSequenceKey = useMemo(() => {
75+
return JSON.stringify(renderSequence.current);
76+
// eslint-disable-next-line react-hooks/exhaustive-deps
77+
}, [renderSequenceVersion]);
78+
7079
const getEventHandler = (id: string, name: string) => {
7180
return eventRegistry.current.get(id)?.get(name);
7281
};
@@ -127,8 +136,23 @@ export const SwiftUIProvider: FunctionComponent<PropsWithChildren<SwiftUIProvide
127136
const getNodes = useCallback(() => nodeRegistry.current, []);
128137

129138
const recordRenderOrder = useCallback((id: string) => {
130-
if (!renderSequence.current.includes(id)) {
131-
renderSequence.current.push(id);
139+
// During render, just append to the sequence - don't trigger state updates
140+
// The sequence will be reset before each render pass in SwiftUIRoot
141+
renderSequence.current.push(id);
142+
}, []);
143+
144+
const commitRenderSequence = useCallback(() => {
145+
// After render, check if order actually changed and only then bump version
146+
const currentSequence = renderSequence.current;
147+
const prevSequence = previousRenderSequence.current;
148+
149+
// Compare sequences - only update if they differ
150+
if (
151+
currentSequence.length !== prevSequence.length ||
152+
currentSequence.some((id, index) => id !== prevSequence[index])
153+
) {
154+
previousRenderSequence.current = [...currentSequence];
155+
setRenderSequenceVersion((prev) => prev + 1);
132156
}
133157
}, []);
134158

@@ -153,9 +177,11 @@ export const SwiftUIProvider: FunctionComponent<PropsWithChildren<SwiftUIProvide
153177
const context = {
154178
getEventHandler,
155179
nodesKey,
180+
renderSequenceKey,
156181
getNodes,
157182
nativeRef,
158183
recordRenderOrder,
184+
commitRenderSequence,
159185
registerEvents,
160186
registerEvent,
161187
registerNode,

0 commit comments

Comments
 (0)