insyd-bottom-sheet 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/SmoothSheet.tsx +45 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insyd-bottom-sheet",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "A smooth, physics-based bottom sheet for React Native — by Ishan Gupta. Works with Expo Go, bare workflow, and React Native CLI.",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -14,7 +14,6 @@ import {
14
14
  GestureDetector,
15
15
  } from "react-native-gesture-handler";
16
16
  import Animated, {
17
- runOnJS,
18
17
  useAnimatedStyle,
19
18
  useSharedValue,
20
19
  withSpring,
@@ -170,9 +169,31 @@ export function SmoothSheet({
170
169
  // Guards against double-dismiss (back button mashing, backdrop tap during close)
171
170
  const isClosing = useRef(false);
172
171
 
172
+ // Unmount is sequenced with a JS-side timer instead of a Reanimated
173
+ // animation-completion callback. runOnJS/scheduleOnRN from the UI thread has
174
+ // a use-after-free race in react-native-worklets 0.10.0 (reanimated #9776):
175
+ // the remote-function registry entry can be freed before the pending call
176
+ // runs, segfaulting — bottom sheets (rapid mount/unmount) are the canonical
177
+ // trigger. The backdrop fade is a fixed 200ms withTiming, so a 220ms timer
178
+ // lands just after it and the swap is invisible.
179
+ const CLOSE_MS = 220;
180
+ const closeTimer = useRef<ReturnType<
181
+ typeof setTimeout
182
+ > | null>(null);
183
+
184
+ function clearCloseTimer() {
185
+ if (closeTimer.current) {
186
+ clearTimeout(closeTimer.current);
187
+ closeTimer.current = null;
188
+ }
189
+ }
190
+
191
+ useEffect(() => clearCloseTimer, []);
192
+
173
193
  useEffect(() => {
174
194
  if (isVisible) {
175
195
  isClosing.current = false;
196
+ clearCloseTimer();
176
197
  setMounted(true);
177
198
  translateY.value = withSpring(0, spring);
178
199
  backdropOpacity.value = withTiming(1, {
@@ -180,13 +201,14 @@ export function SmoothSheet({
180
201
  });
181
202
  } else {
182
203
  translateY.value = withSpring(windowHeight, spring);
183
- backdropOpacity.value = withTiming(
184
- 0,
185
- { duration: 200 },
186
- (finished) => {
187
- if (finished) runOnJS(setMounted)(false);
188
- },
189
- );
204
+ backdropOpacity.value = withTiming(0, {
205
+ duration: 200,
206
+ });
207
+ clearCloseTimer();
208
+ closeTimer.current = setTimeout(() => {
209
+ closeTimer.current = null;
210
+ setMounted(false);
211
+ }, CLOSE_MS);
190
212
  }
191
213
  // eslint-disable-next-line react-hooks/exhaustive-deps
192
214
  }, [isVisible]);
@@ -195,16 +217,15 @@ export function SmoothSheet({
195
217
  if (isClosing.current) return;
196
218
  isClosing.current = true;
197
219
  translateY.value = withSpring(windowHeight, spring);
198
- backdropOpacity.value = withTiming(
199
- 0,
200
- { duration: 200 },
201
- (finished) => {
202
- if (finished) {
203
- runOnJS(setMounted)(false);
204
- runOnJS(onDismiss)();
205
- }
206
- },
207
- );
220
+ backdropOpacity.value = withTiming(0, {
221
+ duration: 200,
222
+ });
223
+ clearCloseTimer();
224
+ closeTimer.current = setTimeout(() => {
225
+ closeTimer.current = null;
226
+ setMounted(false);
227
+ onDismiss();
228
+ }, CLOSE_MS);
208
229
  }
209
230
 
210
231
  // Android hardware back closes the sheet (replaces Modal's onRequestClose).
@@ -221,7 +242,12 @@ export function SmoothSheet({
221
242
  // eslint-disable-next-line react-hooks/exhaustive-deps
222
243
  }, [mounted]);
223
244
 
245
+ // runOnJS(true): callbacks run on the JS thread, so no worklet→JS
246
+ // remote-function crossing (see the worklets 0.10.0 use-after-free note
247
+ // above). Setting a shared value from JS still animates on the UI thread;
248
+ // for a drag handle the one-frame latency is imperceptible.
224
249
  const panGesture = Gesture.Pan()
250
+ .runOnJS(true)
225
251
  .onUpdate((e) => {
226
252
  if (e.translationY > 0) {
227
253
  translateY.value = e.translationY;
@@ -232,7 +258,7 @@ export function SmoothSheet({
232
258
  e.translationY > dismissThreshold ||
233
259
  e.velocityY > dismissVelocityThreshold
234
260
  ) {
235
- runOnJS(dismiss)();
261
+ dismiss();
236
262
  } else {
237
263
  translateY.value = withSpring(0, spring);
238
264
  }