Skip to content

Commit 1401694

Browse files
authored
Fix state transition flowchart docs (#3580)
## Description This PR fixes: - Malformed flowchart in the state transition documentation. - Infinite re-rendering of the flowchart component. - Multiple warnings and errors due to package version changes. This PR is only meant fix the recently introduced malformations as well as the worst of the issues with this component. Before|After ----|---- <img width="899" alt="image" src="https://github.com/user-attachments/assets/afc7d3f9-e8aa-4cdc-9ebe-da8259570d9c" />|<img width="881" alt="image" src="https://github.com/user-attachments/assets/6e1ac7f4-75c5-4a3c-b7b6-9311e69bf5aa" /> ## Test plan - Start docs: `cd packages/docs-gesture-handler; yarn; yarn start;` - Open `/react-native-gesture-handler/docs/fundamentals/states-events/` docs page.
1 parent 816dd4e commit 1401694

File tree

4 files changed

+114
-98
lines changed

4 files changed

+114
-98
lines changed

packages/docs-gesture-handler/src/examples/GestureStateFlowExample/ChartItem.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,39 @@
11
import { Grid } from '@mui/material';
2-
import React, { LegacyRef, useEffect } from 'react';
3-
import { StyleProp, StyleSheet, View, Text } from 'react-native';
2+
import { useEffect, useLayoutEffect, useRef } from 'react';
3+
import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
44
import ChartManager, { Item, WAVE_DELAY_MS } from './ChartManager';
55
import Animated, {
66
useAnimatedStyle,
77
useSharedValue,
88
withSpring,
99
} from 'react-native-reanimated';
1010

11+
export type Coordinate = {
12+
x: number;
13+
y: number;
14+
};
15+
1116
interface ChartItemProps {
1217
item: Item;
1318
chartManager: ChartManager;
14-
innerRef?: LegacyRef<View>;
15-
style?: StyleProp<any>;
19+
updateCoordinates?: (id: number, coordinate: Coordinate) => void;
20+
style?: StyleProp<ViewStyle>;
1621
}
1722

23+
const getCenter = (side: number, size: number) => side + size / 2;
24+
1825
export default function ChartItem({
1926
item,
2027
chartManager,
21-
innerRef,
28+
updateCoordinates,
2229
style,
2330
}: ChartItemProps) {
31+
const ref = useRef<View>(null);
32+
2433
const progress = useSharedValue(0);
2534

2635
useEffect(() => {
27-
if (item.id != ChartManager.EMPTY_SPACE_ID) {
36+
if (item.id !== ChartManager.EMPTY_SPACE_ID) {
2837
const listenerId = chartManager.addListener(item.id, (isActive) => {
2938
progress.value = withSpring(isActive ? 1 : 0, {
3039
duration: 2 * WAVE_DELAY_MS,
@@ -35,7 +44,7 @@ export default function ChartItem({
3544
chartManager.removeListener(item.id, listenerId);
3645
};
3746
}
38-
}, [chartManager]);
47+
}, [chartManager, item.id, progress]);
3948

4049
const animatedStyle = useAnimatedStyle(() => {
4150
return {
@@ -56,16 +65,31 @@ export default function ChartItem({
5665
};
5766
});
5867

68+
useLayoutEffect(() => {
69+
const box = (
70+
ref.current as unknown as HTMLElement
71+
)?.getBoundingClientRect?.();
72+
73+
if (!box) {
74+
return; // no-op on undefined view ref
75+
}
76+
77+
updateCoordinates(item.id, {
78+
x: getCenter(box.left, box.width),
79+
y: getCenter(box.top, box.height),
80+
});
81+
}, [item, updateCoordinates]);
82+
5983
return (
60-
<Grid item style={styles.box} xs={3}>
84+
<Grid style={styles.box} size={3}>
6185
<Animated.View
6286
style={[
6387
styles.item,
6488
item.isVisible ? null : styles.hidden,
6589
animatedStyle,
6690
style,
6791
]}
68-
ref={innerRef}>
92+
ref={ref}>
6993
<Animated.Text style={[animatedTextStyle, styles.label, style]}>
7094
{item.label}
7195
</Animated.Text>

packages/docs-gesture-handler/src/examples/GestureStateFlowExample/ChartManager.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useMemo } from 'react';
21
import {
32
TapGesture,
43
PanGesture,
@@ -92,7 +91,7 @@ export default class ChartManager {
9291
private _connections: ChartConnection[] = [];
9392
private _layout: number[][];
9493
private _listeners: Map<number, Map<number, (isActive: boolean) => void>> =
95-
useMemo(() => new Map(), []);
94+
new Map();
9695

9796
public static EMPTY_SPACE_ID = 0;
9897

@@ -120,7 +119,7 @@ export default class ChartManager {
120119
itemId: number,
121120
listener: (isActive: boolean) => void
122121
): number {
123-
const listenerId = this._listeners.get(itemId)?.size - 1 ?? 0;
122+
const listenerId = this._listeners.get(itemId)?.size - 1;
124123

125124
// another map is used inside of _listeners to seamlessly remove listening functions from _listeners
126125
if (this._listeners.has(itemId)) {
@@ -151,7 +150,7 @@ export default class ChartManager {
151150
label = stateToName.get(label);
152151
}
153152

154-
let highlightColor = labelColorMap.get(label) ?? Colors.YELLOW;
153+
const highlightColor = labelColorMap.get(label) ?? Colors.YELLOW;
155154

156155
const newItem = {
157156
id: newId,
@@ -205,12 +204,12 @@ export default class ChartManager {
205204

206205
undeterminedCallback(true);
207206

208-
const resetAllStates = (event: GestureStateChangeEvent<any>) => {
207+
const resetAllStates = (event: GestureStateChangeEvent<unknown>) => {
209208
undeterminedCallback(true);
210-
if (event.state == State.FAILED) {
209+
if (event.state === State.FAILED) {
211210
failedCallback(true);
212211
}
213-
if (event.state == State.CANCELLED) {
212+
if (event.state === State.CANCELLED) {
214213
cancelledCallback(true);
215214
}
216215
setTimeout(() => {
@@ -236,7 +235,7 @@ export default class ChartManager {
236235
.onEnd(() => {
237236
endCallback(true);
238237
})
239-
.onFinalize((event: GestureStateChangeEvent<any>) => {
238+
.onFinalize((event: GestureStateChangeEvent<unknown>) => {
240239
resetAllStates(event);
241240
});
242241

packages/docs-gesture-handler/src/examples/GestureStateFlowExample/FlowChart.tsx

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,49 @@
1-
import 'react-native-gesture-handler';
2-
import React, { useEffect, useRef, useState } from 'react';
1+
import { useCallback, useMemo, useReducer, useRef } from 'react';
32
import { StyleSheet, View } from 'react-native';
43
import ChartManager from './ChartManager';
54
import { Grid } from '@mui/material';
6-
import ChartItem from './ChartItem';
5+
import ChartItem, { Coordinate } from './ChartItem';
76
import Arrow from './Arrow';
87

9-
type Coordinate = {
10-
x: number;
11-
y: number;
12-
};
13-
148
type FlowChartProps = {
159
chartManager: ChartManager;
1610
};
1711

1812
export default function FlowChart({ chartManager }: FlowChartProps) {
19-
const itemsRef = useRef([]);
20-
const itemsCoordsRef = useRef([]);
21-
const rootRef = useRef(null);
22-
23-
// there's a bug where arrows are not shown on the first render on production build
24-
// i hate this but it forces a re-render after the component is mounted
25-
// a man's gotta do what a man's gotta do
26-
const [counter, setCounter] = useState(0);
27-
useEffect(() => {
28-
const timeout = setTimeout(() => {
29-
setCounter(counter + 1);
30-
}, 0);
31-
return () => clearTimeout(timeout);
32-
}, []);
13+
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
14+
const coordinates = useMemo<Map<number, Coordinate>>(() => new Map(), []);
15+
const rootRef = useRef<View>(null);
3316

34-
const getCenter = (side: number, size: number) => side + size / 2;
17+
const updateCoordinates = useCallback(
18+
(id: number, coordinate: Coordinate) => {
19+
const htmlRootElement = rootRef.current as unknown as HTMLElement;
20+
const root = htmlRootElement.getBoundingClientRect();
3521

36-
itemsCoordsRef.current = itemsRef.current.map((element) => {
37-
// during unloading or overresizing, item may reload itself, causing it to be undefined
38-
if (!element) {
39-
return {
40-
x: 0,
41-
y: 0,
42-
} as Coordinate;
43-
}
22+
if (!root) {
23+
return;
24+
}
4425

45-
const box = element.getBoundingClientRect();
46-
const root = rootRef.current.getBoundingClientRect();
47-
return {
48-
x: getCenter(box.left, box.width) - root.left,
49-
y: getCenter(box.top, box.height) - root.top,
50-
} as Coordinate;
51-
});
26+
// Adjust to root relative positioning
27+
coordinates.set(id, {
28+
x: coordinate.x - root.left,
29+
y: coordinate.y - root.top,
30+
});
31+
forceUpdate();
32+
},
33+
[coordinates]
34+
);
5235

5336
return (
5437
<View style={styles.container} ref={rootRef}>
5538
<Grid container rowGap={4}>
56-
{chartManager.layout.map((row, index) => (
57-
<Grid container spacing={4} key={index}>
39+
{chartManager.layout?.map((row) => (
40+
<Grid container width={'100%'} spacing={4} key={row.toString()}>
5841
{row
5942
.map((itemId) => chartManager.items[itemId])
60-
.map((item, index) => (
43+
.map((item) => (
6144
<ChartItem
62-
key={index}
63-
innerRef={(el) => (itemsRef.current[item.id] = el)}
45+
key={item.id}
46+
updateCoordinates={updateCoordinates}
6447
item={item}
6548
chartManager={chartManager}
6649
/>
@@ -72,21 +55,22 @@ export default function FlowChart({ chartManager }: FlowChartProps) {
7255
// we have all the connections layed out,
7356
// but the user may choose not to use some of the available items,
7457
if (
75-
!itemsCoordsRef.current[connection.from] ||
76-
!itemsCoordsRef.current[connection.to]
58+
!coordinates.get(connection.from) ||
59+
!coordinates.get(connection.to)
7760
) {
7861
return <View key={connection.id} />;
7962
}
63+
8064
return (
8165
<Arrow
8266
key={connection.id}
8367
startPoint={{
84-
x: itemsCoordsRef.current[connection.from].x,
85-
y: itemsCoordsRef.current[connection.from].y,
68+
x: coordinates.get(connection.from).x,
69+
y: coordinates.get(connection.from).y,
8670
}}
8771
endPoint={{
88-
x: itemsCoordsRef.current[connection.to].x,
89-
y: itemsCoordsRef.current[connection.to].y,
72+
x: coordinates.get(connection.to).x,
73+
y: coordinates.get(connection.to).y,
9074
}}
9175
/>
9276
);

packages/docs-gesture-handler/src/examples/GestureStateFlowExample/index.tsx

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import 'react-native-gesture-handler';
2-
import React, { useEffect, useMemo, useRef } from 'react';
1+
import { useEffect, useMemo, useReducer } from 'react';
32
import { StyleSheet, View, useWindowDimensions, Text } from 'react-native';
43
import Animated, {
54
useAnimatedStyle,
@@ -19,49 +18,59 @@ import FlowChart from './FlowChart';
1918
const MIN_DESKTOP_WIDTH = 1298;
2019

2120
export default function App() {
22-
const chartManager = useRef(new ChartManager());
21+
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
22+
const chartManager = useMemo(() => new ChartManager(), []);
2323

2424
const [panHandle, capturedPan, resetPan] = useMemo(
25-
() => chartManager.current.newGesture(Gesture.Pan()),
26-
[]
25+
() => chartManager.newGesture(Gesture.Pan()),
26+
[chartManager]
2727
);
2828

2929
const [pressHandle, capturedPress, resetLongPress] = useMemo(
30-
() => chartManager.current.newGesture(Gesture.LongPress()),
31-
[]
30+
() => chartManager.newGesture(Gesture.LongPress()),
31+
[chartManager]
3232
);
3333

34-
useEffect(() => {
35-
resetPan();
36-
resetLongPress();
37-
}, []);
38-
39-
const panIds = panHandle.idObject;
40-
const pressIds = pressHandle.idObject;
41-
4234
const dimensions = useWindowDimensions();
4335
const isDesktopMode = dimensions.width > MIN_DESKTOP_WIDTH;
4436

45-
// prettier-ignore
46-
const desktopLayout = [
47-
[panIds.undetermined, ChartManager.EMPTY_SPACE_ID, pressIds.undetermined, ChartManager.EMPTY_SPACE_ID],
48-
[panIds.began, panIds.failed, pressIds.began, pressIds.failed],
49-
[panIds.active, panIds.cancelled, pressIds.active, pressIds.cancelled],
50-
[panIds.end, ChartManager.EMPTY_SPACE_ID, pressIds.end, ChartManager.EMPTY_SPACE_ID],
51-
];
37+
useEffect(() => {
38+
// Timing issue, neither useEffect, useLayoutEffect or requestAnimationFrame work
39+
const timeout = setTimeout(() => {
40+
resetPan();
41+
resetLongPress();
42+
}, 300);
5243

53-
// prettier-ignore
54-
const phoneLayout = [
55-
[panIds.undetermined],
56-
[panIds.began, panIds.failed],
57-
[panIds.active, panIds.cancelled],
58-
[panIds.end, ChartManager.EMPTY_SPACE_ID],
59-
];
44+
return () => {
45+
clearTimeout(timeout);
46+
};
47+
}, [resetLongPress, resetPan]);
6048

61-
chartManager.current.layout = isDesktopMode ? desktopLayout : phoneLayout;
49+
useEffect(() => {
50+
const panIds = panHandle.idObject;
51+
const pressIds = pressHandle.idObject;
52+
53+
// prettier-ignore
54+
const desktopLayout = [
55+
[panIds.undetermined, ChartManager.EMPTY_SPACE_ID, pressIds.undetermined, ChartManager.EMPTY_SPACE_ID],
56+
[panIds.began, panIds.failed, pressIds.began, pressIds.failed],
57+
[panIds.active, panIds.cancelled, pressIds.active, pressIds.cancelled],
58+
[panIds.end, ChartManager.EMPTY_SPACE_ID, pressIds.end, ChartManager.EMPTY_SPACE_ID],
59+
];
60+
61+
// prettier-ignore
62+
const phoneLayout = [
63+
[panIds.undetermined],
64+
[panIds.began, panIds.failed],
65+
[panIds.active, panIds.cancelled],
66+
[panIds.end, ChartManager.EMPTY_SPACE_ID],
67+
];
68+
69+
chartManager.layout = isDesktopMode ? desktopLayout : phoneLayout;
70+
forceUpdate();
71+
}, [chartManager, isDesktopMode, panHandle, pressHandle]);
6272

6373
const pressed = useSharedValue(false);
64-
6574
const offset = useSharedValue(0);
6675
const scale = useSharedValue(1);
6776

@@ -110,7 +119,7 @@ export default function App() {
110119
<Text style={styles.label}>Gesture.LongPress()</Text>
111120
)}
112121
</View>
113-
<FlowChart chartManager={chartManager.current} />
122+
<FlowChart chartManager={chartManager} />
114123
</View>
115124
<GestureHandlerRootView style={styles.container}>
116125
<View style={styles.container}>

0 commit comments

Comments
 (0)