Skip to content

Commit 29ad9e8

Browse files
authored
refactor: don't use private reanimated API (#1209)
## 📜 Description Added a support for Expo snack. ## 💡 Motivation and Context In this PR I re-worked the approach how I attach worklet handlers to `KeyboardControllerView`. Prior to #538 we simply broadcasted events for stored worklet handlers inside a global object. But this approach had downsides, such as slow flow of execution, so I re-worked it in #538 and instantly introduced issues like: #551 and #555 While I managed somehow to fix it, the new approach wasn't ideal: - we still used internal API that was relying on FS structure; - this is highly internal API that can be changed anytime and will not assure that lib works across all reanimated versions; - in Expo Go I see strange errors, but I tend to think they are derived from the same issue - my code can not lookup a module and we get a crash 🤷‍♂️ I kept the idea to re-work this piece of code and in this PR I'm doing it. I can not use public API only, but the new approach is actually smarter. The new idea is that we **still** use `registerEvent` Should fix errors like: <img width="600" height="1266" alt="image" src="https://github.com/user-attachments/assets/272de55e-91a5-443a-b252-32e5a0f1a50c" /> Actually correct solution for: #555 In future I'll be able to add interactive code samples into documentation, so people can run demos straight in the browser. ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### JS - remove `event-handler` files; - remove `event-mapping` file; - use `useEvent`/`useHandler` hook for `useKeyboardHandler` and `useFocusedInputHandler` hooks; - use `registerForEvents`/`unregisterFromEvents` methods from `WorkletEventHandler` (instance returned by `useEvent` hook); > I also had to use `never` types, or cast to my types using `as unknown as`, since I anyway access reanimated internals. But it's safer to use internals in this way, than before 🤞 ## 🤔 How Has This Been Tested? Tested manually on: - iPhone 16 Pro (new arch, iOS 26.0); - e2e tests; ## 📸 Screenshots (if appropriate): <img width="1721" height="878" alt="image" src="https://github.com/user-attachments/assets/30c8dc65-a7f2-4682-838f-aab4c22189fc" /> ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
1 parent 14471f9 commit 29ad9e8

File tree

8 files changed

+110
-101
lines changed

8 files changed

+110
-101
lines changed

src/animated.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Reanimated, { useSharedValue } from "react-native-reanimated";
99

1010
import { KeyboardControllerView } from "./bindings";
1111
import { KeyboardContext } from "./context";
12-
import { focusedInputEventsMap, keyboardEventsMap } from "./event-mappings";
1312
import { useAnimatedValue, useEventHandlerRegistration } from "./internal";
1413
import { KeyboardController } from "./module";
1514
import {
@@ -19,10 +18,8 @@ import {
1918

2019
import type { KeyboardAnimationContext } from "./context";
2120
import type {
22-
FocusedInputHandler,
2321
FocusedInputLayoutChangedEvent,
2422
KeyboardControllerProps,
25-
KeyboardHandler,
2623
NativeEvent,
2724
} from "./types";
2825
import type { ViewStyle } from "react-native";
@@ -130,14 +127,8 @@ export const KeyboardProvider = (props: KeyboardProviderProps) => {
130127
const progressSV = useSharedValue(0);
131128
const heightSV = useSharedValue(0);
132129
const layout = useSharedValue<FocusedInputLayoutChangedEvent | null>(null);
133-
const setKeyboardHandlers = useEventHandlerRegistration<KeyboardHandler>(
134-
keyboardEventsMap,
135-
viewTagRef,
136-
);
137-
const setInputHandlers = useEventHandlerRegistration<FocusedInputHandler>(
138-
focusedInputEventsMap,
139-
viewTagRef,
140-
);
130+
const setKeyboardHandlers = useEventHandlerRegistration(viewTagRef);
131+
const setInputHandlers = useEventHandlerRegistration(viewTagRef);
141132
// memo
142133
const context = useMemo<KeyboardAnimationContext>(
143134
() => ({

src/context.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { createContext, useContext } from "react";
22
import { Animated } from "react-native";
33

4-
import type {
5-
FocusedInputHandler,
6-
FocusedInputLayoutChangedEvent,
7-
KeyboardHandler,
8-
} from "./types";
4+
import type { FocusedInputLayoutChangedEvent } from "./types";
95
import type React from "react";
10-
import type { SharedValue } from "react-native-reanimated";
6+
import type {
7+
EventHandlerProcessed,
8+
SharedValue,
9+
} from "react-native-reanimated";
1110

1211
export type AnimatedContext = {
1312
/**
@@ -37,9 +36,13 @@ export type KeyboardAnimationContext = {
3736
/** Layout of the focused `TextInput` represented as `SharedValue`. */
3837
layout: SharedValue<FocusedInputLayoutChangedEvent | null>;
3938
/** Method for setting workletized keyboard handlers. */
40-
setKeyboardHandlers: (handlers: KeyboardHandler) => () => void;
39+
setKeyboardHandlers: (
40+
handlers: EventHandlerProcessed<never, never>,
41+
) => () => void;
4142
/** Method for setting workletized handlers for tracking focused input events. */
42-
setInputHandlers: (handlers: FocusedInputHandler) => () => void;
43+
setInputHandlers: (
44+
handlers: EventHandlerProcessed<never, never>,
45+
) => () => void;
4346
/** Method to enable/disable KeyboardController library. */
4447
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
4548
};

src/event-handler.d.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/event-handler.js

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/event-handler.web.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/event-mappings.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/hooks/index.ts

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { useEffect, useLayoutEffect } from "react";
2+
import { useEvent, useHandler } from "react-native-reanimated";
23

34
import { AndroidSoftInputModes } from "../constants";
45
import { useKeyboardContext } from "../context";
56
import { KeyboardController } from "../module";
67

78
import type { AnimatedContext, ReanimatedContext } from "../context";
8-
import type { FocusedInputHandler, KeyboardHandler } from "../types";
9-
import type { DependencyList } from "react";
9+
import type {
10+
FocusedInputHandler,
11+
FocusedInputSelectionChangedEvent,
12+
FocusedInputTextChangedEvent,
13+
KeyboardHandler,
14+
NativeEvent,
15+
} from "../types";
1016

1117
/**
1218
* Hook that sets the Android soft input mode to adjust resize on mount and
@@ -107,12 +113,43 @@ export const useReanimatedKeyboardAnimation = (): ReanimatedContext => {
107113
*/
108114
export function useGenericKeyboardHandler(
109115
handler: KeyboardHandler,
110-
deps?: DependencyList,
116+
deps?: unknown[],
111117
) {
112118
const context = useKeyboardContext();
113119

120+
const { doDependenciesDiffer } = useHandler(handler, deps);
121+
122+
const eventHandler = useEvent<NativeEvent>(
123+
(event) => {
124+
"worklet";
125+
126+
if (event.eventName.endsWith("onKeyboardMoveStart")) {
127+
handler.onStart?.(event);
128+
}
129+
130+
if (event.eventName.endsWith("onKeyboardMove")) {
131+
handler.onMove?.(event);
132+
}
133+
134+
if (event.eventName.endsWith("onKeyboardMoveEnd")) {
135+
handler.onEnd?.(event);
136+
}
137+
138+
if (event.eventName.endsWith("onKeyboardMoveInteractive")) {
139+
handler.onInteractive?.(event);
140+
}
141+
},
142+
[
143+
"onKeyboardMoveStart",
144+
"onKeyboardMove",
145+
"onKeyboardMoveEnd",
146+
"onKeyboardMoveInteractive",
147+
],
148+
doDependenciesDiffer,
149+
);
150+
114151
useLayoutEffect(() => {
115-
const cleanup = context.setKeyboardHandlers(handler);
152+
const cleanup = context.setKeyboardHandlers(eventHandler);
116153

117154
return () => cleanup();
118155
}, deps);
@@ -149,10 +186,7 @@ export function useGenericKeyboardHandler(
149186
* }
150187
* ```
151188
*/
152-
export function useKeyboardHandler(
153-
handler: KeyboardHandler,
154-
deps?: DependencyList,
155-
) {
189+
export function useKeyboardHandler(handler: KeyboardHandler, deps?: unknown[]) {
156190
useResizeMode();
157191
useGenericKeyboardHandler(handler, deps);
158192
}
@@ -224,12 +258,32 @@ export function useReanimatedFocusedInput() {
224258
*/
225259
export function useFocusedInputHandler(
226260
handler: FocusedInputHandler,
227-
deps?: DependencyList,
261+
deps?: unknown[],
228262
) {
229263
const context = useKeyboardContext();
230264

265+
const { doDependenciesDiffer } = useHandler<never, never>(handler, deps);
266+
267+
const eventHandler = useEvent<
268+
FocusedInputSelectionChangedEvent | FocusedInputTextChangedEvent
269+
>(
270+
(event) => {
271+
"worklet";
272+
273+
if (event.eventName.endsWith("onFocusedInputTextChanged")) {
274+
handler.onChangeText?.(event as FocusedInputTextChangedEvent);
275+
}
276+
277+
if (event.eventName.endsWith("onFocusedInputSelectionChanged")) {
278+
handler.onSelectionChange?.(event as FocusedInputSelectionChangedEvent);
279+
}
280+
},
281+
["onFocusedInputTextChanged", "onFocusedInputSelectionChanged"],
282+
doDependenciesDiffer,
283+
);
284+
231285
useLayoutEffect(() => {
232-
const cleanup = context.setInputHandlers(handler);
286+
const cleanup = context.setInputHandlers(eventHandler);
233287

234288
return () => cleanup();
235289
}, deps);

src/internal.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
import { useRef } from "react";
22
import { Animated } from "react-native";
33

4-
import { registerEventHandler, unregisterEventHandler } from "./event-handler";
54
import { findNodeHandle } from "./utils/findNodeHandle";
65

7-
type EventHandler = (event: never) => void;
6+
import type { EventHandlerProcessed } from "react-native-reanimated";
7+
88
type ComponentOrHandle = Parameters<typeof findNodeHandle>[0];
99

10+
type WorkletHandler = {
11+
registerForEvents: (viewTag: number) => void;
12+
unregisterFromEvents: (viewTag: number) => void;
13+
};
14+
15+
type WorkletHandlerOrWorkletHandlerObject =
16+
| WorkletHandler
17+
| {
18+
workletEventHandler: WorkletHandler;
19+
};
20+
1021
/**
1122
* An internal hook that helps to register workletized event handlers.
1223
*
13-
* @param map - Map of event handlers and their names.
1424
* @param viewTagRef - Ref to the view that produces events.
1525
* @returns A function that registers supplied event handlers.
1626
* @example
@@ -21,14 +31,12 @@ type ComponentOrHandle = Parameters<typeof findNodeHandle>[0];
2131
* );
2232
* ```
2333
*/
24-
export function useEventHandlerRegistration<
25-
H extends Partial<Record<string, EventHandler>>,
26-
>(
27-
map: Map<keyof H, string>,
34+
export function useEventHandlerRegistration(
2835
viewTagRef: React.MutableRefObject<ComponentOrHandle>,
2936
) {
30-
const onRegisterHandler = (handler: H) => {
31-
const ids: (number | null)[] = [];
37+
const onRegisterHandler = (handler: EventHandlerProcessed<never, never>) => {
38+
const currentHandler =
39+
handler as unknown as WorkletHandlerOrWorkletHandlerObject;
3240
const attachWorkletHandlers = () => {
3341
const viewTag = findNodeHandle(viewTagRef.current);
3442

@@ -38,26 +46,13 @@ export function useEventHandlerRegistration<
3846
);
3947
}
4048

41-
ids.push(
42-
...Object.keys(handler).map((handlerName) => {
43-
const eventName = map.get(handlerName as keyof H);
44-
const functionToCall = handler[handlerName as keyof H];
45-
46-
if (eventName && viewTag) {
47-
return registerEventHandler(
48-
(event: Parameters<NonNullable<H[keyof H]>>[0]) => {
49-
"worklet";
50-
51-
functionToCall?.(event);
52-
},
53-
eventName,
54-
viewTag,
55-
);
56-
}
57-
58-
return null;
59-
}),
60-
);
49+
if (viewTag) {
50+
if ("workletEventHandler" in currentHandler) {
51+
currentHandler.workletEventHandler.registerForEvents(viewTag);
52+
} else {
53+
currentHandler.registerForEvents(viewTag);
54+
}
55+
}
6156
};
6257

6358
if (viewTagRef.current) {
@@ -68,7 +63,15 @@ export function useEventHandlerRegistration<
6863
}
6964

7065
return () => {
71-
ids.forEach((id) => (id ? unregisterEventHandler(id) : null));
66+
const viewTag = findNodeHandle(viewTagRef.current);
67+
68+
if (viewTag) {
69+
if ("workletEventHandler" in currentHandler) {
70+
currentHandler.workletEventHandler.unregisterFromEvents(viewTag);
71+
} else {
72+
currentHandler.unregisterFromEvents(viewTag);
73+
}
74+
}
7275
};
7376
};
7477

0 commit comments

Comments
 (0)