Migrate runOnUI/runOnJS to scheduleOnUI/scheduleOnRN (#11249)

This commit is contained in:
Tomasz Zawadzki
2026-07-26 21:00:35 +02:00
committed by GitHub
parent d5e335e575
commit 2b9deeeb6a
24 changed files with 102 additions and 109 deletions
+2 -2
View File
@@ -9,13 +9,13 @@ import {
import Animated, { import Animated, {
Easing, Easing,
interpolate, interpolate,
runOnJS,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Svg, {Path, type SvgProps} from 'react-native-svg' import Svg, {Path, type SvgProps} from 'react-native-svg'
import {scheduleOnRN} from 'react-native-worklets'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import * as SplashScreen from 'expo-splash-screen' import * as SplashScreen from 'expo-splash-screen'
@@ -136,7 +136,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
1, 1,
{duration: 1200, easing: Easing.in(Easing.cubic)}, {duration: 1200, easing: Easing.in(Easing.cubic)},
() => { () => {
runOnJS(onFinish)() scheduleOnRN(onFinish)
}, },
), ),
) )
+5 -5
View File
@@ -30,7 +30,6 @@ import {KeyboardEvents} from 'react-native-keyboard-controller'
import Animated, { import Animated, {
clamp, clamp,
interpolate, interpolate,
runOnJS,
type SharedValue, type SharedValue,
useAnimatedReaction, useAnimatedReaction,
useAnimatedStyle, useAnimatedStyle,
@@ -44,6 +43,7 @@ import {
useSafeAreaInsets, useSafeAreaInsets,
} from 'react-native-safe-area-context' } from 'react-native-safe-area-context'
import {captureRef} from 'react-native-view-shot' import {captureRef} from 'react-native-view-shot'
import {scheduleOnRN} from 'react-native-worklets'
import {Image, type ImageErrorEventData} from 'expo-image' import {Image, type ImageErrorEventData} from 'expo-image'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -166,7 +166,7 @@ export function Root({children}: {children: React.ReactNode}) {
// note: return location has to be reset on open, // note: return location has to be reset on open,
// rather than on close, otherwise there's a flicker // rather than on close, otherwise there's a flicker
// where the reanimated update is faster than the react render // where the reanimated update is faster than the react render
runOnJS(onCompletedClose)() scheduleOnRN(onCompletedClose)
} }
}), }),
) )
@@ -331,7 +331,7 @@ export function Trigger({
() => hoveredItemSV.get(), () => hoveredItemSV.get(),
(hovered, prev) => { (hovered, prev) => {
if (hovered !== prev) { if (hovered !== prev) {
runOnJS(setHoveredMenuItem)(hovered) scheduleOnRN(setHoveredMenuItem, hovered)
} }
}, },
) )
@@ -343,7 +343,7 @@ export function Trigger({
.averageTouches(true) .averageTouches(true)
.onStart(() => { .onStart(() => {
'worklet' 'worklet'
runOnJS(open)('full') scheduleOnRN(open, 'full')
}) })
.onUpdate(evt => { .onUpdate(evt => {
'worklet' 'worklet'
@@ -357,7 +357,7 @@ export function Trigger({
// as the menu may have slid into place beneath their finger // as the menu may have slid into place beneath their finger
const item = hoveredItemSV.get() const item = hoveredItemSV.get()
if (item) { if (item) {
runOnJS(onTouchUpMenuItem)(item) scheduleOnRN(onTouchUpMenuItem, item)
} }
}) })
}, [open, hoverablesSV, onTouchUpMenuItem, hoveredItemSV, translationSV]) }, [open, hoverablesSV, onTouchUpMenuItem, hoveredItemSV, translationSV])
+3 -3
View File
@@ -21,11 +21,11 @@ import {
} from 'react-native' } from 'react-native'
import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller' import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller'
import Animated, { import Animated, {
runOnJS,
type ScrollEvent, type ScrollEvent,
useAnimatedStyle, useAnimatedStyle,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -299,9 +299,9 @@ export const InnerFlatList = forwardRef<
} }
const {contentOffset} = e const {contentOffset} = e
if (contentOffset.y > 0 && !disableDrag) { if (contentOffset.y > 0 && !disableDrag) {
runOnJS(setDisableDrag)(true) scheduleOnRN(setDisableDrag, true)
} else if (contentOffset.y <= 1 && disableDrag) { } else if (contentOffset.y <= 1 && disableDrag) {
runOnJS(setDisableDrag)(false) scheduleOnRN(setDisableDrag, false)
} }
} }
+8 -8
View File
@@ -4,7 +4,6 @@ import Animated, {
type AnimatedRef, type AnimatedRef,
measure, measure,
Reanimated3DefaultSpringConfig, Reanimated3DefaultSpringConfig,
runOnJS,
scrollTo, scrollTo,
type SharedValue, type SharedValue,
useAnimatedRef, useAnimatedRef,
@@ -14,6 +13,7 @@ import Animated, {
withSpring, withSpring,
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {atoms as a, useTheme, web} from '#/alf' import {atoms as a, useTheme, web} from '#/alf'
@@ -26,7 +26,7 @@ import {IS_IOS} from '#/env'
* *
* All positioning is driven by a `slots` map (key → index) and translateY * All positioning is driven by a `slots` map (key → index) and translateY
* (no discrete `top` changes). On drag end the new slot assignment is * (no discrete `top` changes). On drag end the new slot assignment is
* computed on the UI thread first, then React state is updated via runOnJS. * computed on the UI thread first, then React state is updated via scheduleOnRN.
* *
* See SortableList.web.tsx for the web implementation using pointer events. * See SortableList.web.tsx for the web implementation using pointer events.
*/ */
@@ -265,9 +265,9 @@ function SortableItem<T>({
measureDone.set(false) measureDone.set(false)
lastHapticSlot.set(mySlot) lastHapticSlot.set(mySlot)
if (onDragStart) { if (onDragStart) {
runOnJS(onDragStart)() scheduleOnRN(onDragStart)
} }
runOnJS(playHaptic)() scheduleOnRN(playHaptic)
}) })
.onChange(e => { .onChange(e => {
'worklet' 'worklet'
@@ -285,7 +285,7 @@ function SortableItem<T>({
const clampedSlot = Math.max(0, Math.min(currentSlot, itemCount - 1)) const clampedSlot = Math.max(0, Math.min(currentSlot, itemCount - 1))
if (IS_IOS && clampedSlot !== lastHapticSlot.get()) { if (IS_IOS && clampedSlot !== lastHapticSlot.get()) {
lastHapticSlot.set(clampedSlot) lastHapticSlot.set(clampedSlot)
runOnJS(playHaptic)('Light') scheduleOnRN(playHaptic, 'Light')
} }
}) })
.onEnd(() => { .onEnd(() => {
@@ -326,13 +326,13 @@ function SortableItem<T>({
dragStartSlot: -1, dragStartSlot: -1,
}) })
dragY.set(0) dragY.set(0)
runOnJS(onCommitReorder)(sorted) scheduleOnRN(onCommitReorder, sorted)
} else { } else {
const s = state.get() const s = state.get()
state.set({...s, activeKey: '', dragStartSlot: -1}) state.set({...s, activeKey: '', dragStartSlot: -1})
dragY.set(0) dragY.set(0)
if (onDragEnd) { if (onDragEnd) {
runOnJS(onDragEnd)() scheduleOnRN(onDragEnd)
} }
} }
} }
@@ -347,7 +347,7 @@ function SortableItem<T>({
const s = state.get() const s = state.get()
state.set({...s, activeKey: '', dragStartSlot: -1}) state.set({...s, activeKey: '', dragStartSlot: -1})
if (onDragEnd) { if (onDragEnd) {
runOnJS(onDragEnd)() scheduleOnRN(onDragEnd)
} }
} }
}) })
+2 -2
View File
@@ -2,12 +2,12 @@ import {useRef, useState} from 'react'
import {Modal, Pressable, StyleSheet, View} from 'react-native' import {Modal, Pressable, StyleSheet, View} from 'react-native'
import Animated, { import Animated, {
interpolate, interpolate,
runOnJS,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
withSpring, withSpring,
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
@@ -52,7 +52,7 @@ export function ImageMenu({onPressShare, onPressSave}: Props) {
progress.set( progress.set(
withTiming(0, TIMING_OUT, finished => { withTiming(0, TIMING_OUT, finished => {
if (finished) { if (finished) {
runOnJS(setIsMounted)(false) scheduleOnRN(setIsMounted, false)
} }
}), }),
) )
@@ -8,7 +8,6 @@ import {
import Animated, { import Animated, {
type AnimatableValue, type AnimatableValue,
Reanimated3DefaultSpringConfig, Reanimated3DefaultSpringConfig,
runOnJS,
type SharedValue, type SharedValue,
useAnimatedReaction, useAnimatedReaction,
useAnimatedRef, useAnimatedRef,
@@ -16,6 +15,7 @@ import Animated, {
useSharedValue, useSharedValue,
withSpring, withSpring,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import { import {
@@ -95,7 +95,7 @@ const ImageItem = ({
}, },
(nextIsScaled, prevIsScaled) => { (nextIsScaled, prevIsScaled) => {
if (nextIsScaled !== prevIsScaled) { if (nextIsScaled !== prevIsScaled) {
runOnJS(handleZoom)(nextIsScaled) scheduleOnRN(handleZoom, nextIsScaled)
} }
}, },
) )
@@ -245,7 +245,7 @@ const ImageItem = ({
const singleTap = Gesture.Tap().onEnd(() => { const singleTap = Gesture.Tap().onEnd(() => {
'worklet' 'worklet'
runOnJS(onTap)() scheduleOnRN(onTap)
}) })
const doubleTap = Gesture.Tap() const doubleTap = Gesture.Tap()
@@ -359,9 +359,9 @@ const ImageItem = ({
}, },
(show, prevShow) => { (show, prevShow) => {
if (!prevShow && show) { if (!prevShow && show) {
runOnJS(setShowLoader)(true) scheduleOnRN(setShowLoader, true)
} else if (prevShow && !show) { } else if (prevShow && !show) {
runOnJS(setShowLoader)(false) scheduleOnRN(setShowLoader, false)
} }
}, },
) )
@@ -14,7 +14,6 @@ import {
type PanGesture, type PanGesture,
} from 'react-native-gesture-handler' } from 'react-native-gesture-handler'
import Animated, { import Animated, {
runOnJS,
type SharedValue, type SharedValue,
useAnimatedProps, useAnimatedProps,
useAnimatedReaction, useAnimatedReaction,
@@ -24,6 +23,7 @@ import Animated, {
useSharedValue, useSharedValue,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaFrame} from 'react-native-safe-area-context' import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import { import {
@@ -84,7 +84,7 @@ const ImageItem = ({
'worklet' 'worklet'
const nextIsScaled = e.zoomScale > 1 const nextIsScaled = e.zoomScale > 1
if (scaled !== nextIsScaled) { if (scaled !== nextIsScaled) {
runOnJS(handleZoom)(nextIsScaled) scheduleOnRN(handleZoom, nextIsScaled)
} }
}, },
onBeginDrag() { onBeginDrag() {
@@ -118,7 +118,7 @@ const ImageItem = ({
const singleTap = Gesture.Tap().onEnd(() => { const singleTap = Gesture.Tap().onEnd(() => {
'worklet' 'worklet'
runOnJS(onTap)() scheduleOnRN(onTap)
}) })
const doubleTap = Gesture.Tap() const doubleTap = Gesture.Tap()
@@ -142,7 +142,7 @@ const ImageItem = ({
screenSize, screenSize,
) )
} }
runOnJS(zoomTo)(nextZoomRect) scheduleOnRN(zoomTo, nextZoomRect)
}) })
const composedGesture = Gesture.Exclusive( const composedGesture = Gesture.Exclusive(
@@ -199,9 +199,9 @@ const ImageItem = ({
}, },
(show, prevShow) => { (show, prevShow) => {
if (!prevShow && show) { if (!prevShow && show) {
runOnJS(setShowLoader)(true) scheduleOnRN(setShowLoader, true)
} else if (prevShow && !show) { } else if (prevShow && !show) {
runOnJS(setShowLoader)(false) scheduleOnRN(setShowLoader, false)
} }
}, },
) )
+10 -11
View File
@@ -20,8 +20,6 @@ import Animated, {
measure, measure,
type MeasuredDimensions, type MeasuredDimensions,
ReduceMotion, ReduceMotion,
runOnJS,
runOnUI,
type SharedValue, type SharedValue,
useAnimatedReaction, useAnimatedReaction,
useAnimatedRef, useAnimatedRef,
@@ -32,6 +30,7 @@ import Animated, {
withSpring, withSpring,
type WithSpringConfig, type WithSpringConfig,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import * as ScreenOrientation from 'expo-screen-orientation' import * as ScreenOrientation from 'expo-screen-orientation'
@@ -136,10 +135,10 @@ export default function ImageViewRoot({
const onFullyClosed = useCallback(() => { const onFullyClosed = useCallback(() => {
setActiveLightbox(null) setActiveLightbox(null)
runOnUI(() => { scheduleOnUI(() => {
'worklet' 'worklet'
thumbRects.set({}) thumbRects.set({})
})() })
requestIdleCallback(() => { requestIdleCallback(() => {
void Image.clearMemoryCache() void Image.clearMemoryCache()
}) })
@@ -149,7 +148,7 @@ export default function ImageViewRoot({
() => openProgress.get() === 0, () => openProgress.get() === 0,
(isGone, wasGone) => { (isGone, wasGone) => {
if (isGone && !wasGone) { if (isGone && !wasGone) {
runOnJS(onFullyClosed)() scheduleOnRN(onFullyClosed)
} }
}, },
) )
@@ -160,10 +159,10 @@ export default function ImageViewRoot({
() => openProgress.get() === 1, () => openProgress.get() === 1,
(isOpen, wasOpen) => { (isOpen, wasOpen) => {
if (isOpen && !wasOpen) { if (isOpen && !wasOpen) {
runOnJS(ScreenOrientation.unlockAsync)() scheduleOnRN(ScreenOrientation.unlockAsync)
} else if (!isOpen && wasOpen) { } else if (!isOpen && wasOpen) {
// default is PORTRAIT_UP - set via config plugin in app.config.js -sfn // default is PORTRAIT_UP - set via config plugin in app.config.js -sfn
runOnJS(ScreenOrientation.lockAsync)(PORTRAIT_UP) scheduleOnRN(ScreenOrientation.lockAsync, PORTRAIT_UP)
} }
}, },
) )
@@ -171,7 +170,7 @@ export default function ImageViewRoot({
const onFlyAway = useCallback(() => { const onFlyAway = useCallback(() => {
'worklet' 'worklet'
openProgress.set(0) openProgress.set(0)
runOnJS(onRequestClose)() scheduleOnRN(onRequestClose)
}, [onRequestClose, openProgress]) }, [onRequestClose, openProgress])
return ( return (
@@ -320,7 +319,7 @@ function ImageView({
const handleRequestClose = useCallback(() => { const handleRequestClose = useCallback(() => {
const activeRef = images[imageIndex]?.thumbRef const activeRef = images[imageIndex]?.thumbRef
if (isAnimated && activeRef) { if (isAnimated && activeRef) {
runOnUI(() => { scheduleOnUI(() => {
'worklet' 'worklet'
const rect = measure(activeRef) const rect = measure(activeRef)
thumbRects.modify(rects => { thumbRects.modify(rects => {
@@ -328,8 +327,8 @@ function ImageView({
rects[imageIndex] = rect rects[imageIndex] = rect
return rects return rects
}) })
runOnJS(onRequestClose)() scheduleOnRN(onRequestClose)
})() })
} else { } else {
onRequestClose() onRequestClose()
} }
+6 -10
View File
@@ -1,10 +1,6 @@
import {createContext, useContext, useEffect, useMemo, useState} from 'react' import {createContext, useContext, useEffect, useMemo, useState} from 'react'
import { import {measure, type MeasuredDimensions} from 'react-native-reanimated'
measure, import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets'
type MeasuredDimensions,
runOnJS,
runOnUI,
} from 'react-native-reanimated'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -73,7 +69,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
if (thumbRef) { if (thumbRef) {
// Measure the tapped image on the UI thread, then open with // Measure the tapped image on the UI thread, then open with
// the rect baked in so it's available from the first render. // the rect baked in so it's available from the first render.
// Only the rect (plain data) goes through runOnJS — AnimatedRef // Only the rect (plain data) goes through scheduleOnRN — AnimatedRef
// objects can't survive serialization across threads. // objects can't survive serialization across threads.
const openWithRect = (rect: MeasuredDimensions | null) => { const openWithRect = (rect: MeasuredDimensions | null) => {
doOpen({ doOpen({
@@ -83,11 +79,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
), ),
}) })
} }
runOnUI(() => { scheduleOnUI(() => {
'worklet' 'worklet'
const rect = measure(thumbRef) const rect = measure(thumbRef)
runOnJS(openWithRect)(rect) scheduleOnRN(openWithRect, rect)
})() })
} else { } else {
doOpen(lightbox) doOpen(lightbox)
} }
@@ -8,12 +8,12 @@ import {
} from 'react-native' } from 'react-native'
import Animated, { import Animated, {
measure, measure,
runOnJS,
useAnimatedRef, useAnimatedRef,
useFrameCallback, useFrameCallback,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {WebView} from 'react-native-webview' import {WebView} from 'react-native-webview'
import {scheduleOnRN} from 'react-native-worklets'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {type AppBskyEmbedExternal} from '@atproto/api' import {type AppBskyEmbedExternal} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -164,7 +164,7 @@ export function ExternalPlayer({
const isVisible = top <= realWinHeight - insets.bottom && bot >= insets.top const isVisible = top <= realWinHeight - insets.bottom && bot >= insets.top
if (!isVisible) { if (!isVisible) {
runOnJS(setIsPlayerActive)(false) scheduleOnRN(setIsPlayerActive, false)
} }
}, false) // False here disables autostarting the callback }, false) // False here disables autostarting the callback
+3 -3
View File
@@ -9,12 +9,12 @@ import {
import {Pressable, useWindowDimensions, View} from 'react-native' import {Pressable, useWindowDimensions, View} from 'react-native'
import Animated, { import Animated, {
Easing, Easing,
runOnJS,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -69,7 +69,7 @@ export const ProgressGuideToast = forwardRef<
duration: 400, duration: 400,
easing: Easing.out(Easing.cubic), easing: Easing.out(Easing.cubic),
}, },
() => runOnJS(setIsntOpen)(), () => scheduleOnRN(setIsntOpen),
), ),
) )
}, [setIsOpen, opacity]) }, [setIsOpen, opacity])
@@ -88,7 +88,7 @@ export const ProgressGuideToast = forwardRef<
duration: 100, duration: 100,
easing: Easing.out(Easing.cubic), easing: Easing.out(Easing.cubic),
}, },
() => runOnJS(playCheckmark)(), () => scheduleOnRN(playCheckmark),
), ),
) )
translateY.set(0) translateY.set(0)
+2 -2
View File
@@ -1,12 +1,12 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import Animated, { import Animated, {
runOnJS,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import { import {
ScaleAndFadeIn, ScaleAndFadeIn,
@@ -41,7 +41,7 @@ export function NewMessagesPill({
}, [scale]) }, [scale])
const onPress = useCallback(() => { const onPress = useCallback(() => {
runOnJS(playHaptic)() scheduleOnRN(playHaptic)
onPressInner?.() onPressInner?.()
}, [onPressInner, playHaptic]) }, [onPressInner, playHaptic])
+3 -3
View File
@@ -4,13 +4,13 @@ import {Gesture, type GestureType} from 'react-native-gesture-handler'
import Animated, { import Animated, {
clamp, clamp,
interpolate, interpolate,
runOnJS,
useAnimatedStyle, useAnimatedStyle,
useReducedMotion, useReducedMotion,
useSharedValue, useSharedValue,
withSequence, withSequence,
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {atoms as a, tokens, useTheme} from '#/alf' import {atoms as a, tokens, useTheme} from '#/alf'
@@ -111,7 +111,7 @@ export function SwipeToReply({
if (pastThreshold && !hit.get()) { if (pastThreshold && !hit.get()) {
hit.set(true) hit.set(true)
runPop() runPop()
runOnJS(playHaptic)('Medium') scheduleOnRN(playHaptic, 'Medium')
} else if (!pastThreshold && hit.get()) { } else if (!pastThreshold && hit.get()) {
hit.set(false) hit.set(false)
} }
@@ -120,7 +120,7 @@ export function SwipeToReply({
'worklet' 'worklet'
// Only a clean end (finger lifted past threshold) triggers the reply. // Only a clean end (finger lifted past threshold) triggers the reply.
if (hit.get()) { if (hit.get()) {
runOnJS(onReply)() scheduleOnRN(onReply)
} }
}) })
.onFinalize(() => { .onFinalize(() => {
+14 -14
View File
@@ -5,7 +5,6 @@ import Animated, {
clamp, clamp,
interpolate, interpolate,
interpolateColor, interpolateColor,
runOnJS,
useAnimatedReaction, useAnimatedReaction,
useAnimatedStyle, useAnimatedStyle,
useDerivedValue, useDerivedValue,
@@ -14,6 +13,7 @@ import Animated, {
withSequence, withSequence,
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {type GestureActions} from './GestureActionView.shared' import {type GestureActions} from './GestureActionView.shared'
@@ -74,17 +74,17 @@ export function GestureActionView({
() => transX, () => transX,
() => { () => {
if (transX.get() === 0) { if (transX.get() === 0) {
runOnJS(setActiveAction)(null) scheduleOnRN(setActiveAction, null)
} else if (transX.get() < 0) { } else if (transX.get() < 0) {
if ( if (
actions.leftSecond && actions.leftSecond &&
transX.get() <= -actions.leftSecond.threshold transX.get() <= -actions.leftSecond.threshold
) { ) {
if (activeAction !== 'leftSecond') { if (activeAction !== 'leftSecond') {
runOnJS(setActiveAction)('leftSecond') scheduleOnRN(setActiveAction, 'leftSecond')
} }
} else if (activeAction !== 'leftFirst') { } else if (activeAction !== 'leftFirst') {
runOnJS(setActiveAction)('leftFirst') scheduleOnRN(setActiveAction, 'leftFirst')
} }
} else if (transX.get() > 0) { } else if (transX.get() > 0) {
if ( if (
@@ -92,10 +92,10 @@ export function GestureActionView({
transX.get() > actions.rightSecond.threshold transX.get() > actions.rightSecond.threshold
) { ) {
if (activeAction !== 'rightSecond') { if (activeAction !== 'rightSecond') {
runOnJS(setActiveAction)('rightSecond') scheduleOnRN(setActiveAction, 'rightSecond')
} }
} else if (activeAction !== 'rightFirst') { } else if (activeAction !== 'rightFirst') {
runOnJS(setActiveAction)('rightFirst') scheduleOnRN(setActiveAction, 'rightFirst')
} }
} }
}, },
@@ -127,7 +127,7 @@ export function GestureActionView({
!hitSecond.get() !hitSecond.get()
) { ) {
runPopAnimation() runPopAnimation()
runOnJS(haptic)() scheduleOnRN(haptic)
hitSecond.set(true) hitSecond.set(true)
} else if ( } else if (
hitSecond.get() && hitSecond.get() &&
@@ -144,7 +144,7 @@ export function GestureActionView({
!hitFirst.get() !hitFirst.get()
) { ) {
runPopAnimation() runPopAnimation()
runOnJS(haptic)() scheduleOnRN(haptic)
hitFirst.set(true) hitFirst.set(true)
} else if ( } else if (
hitFirst.get() && hitFirst.get() &&
@@ -161,7 +161,7 @@ export function GestureActionView({
!hitSecond.get() !hitSecond.get()
) { ) {
runPopAnimation() runPopAnimation()
runOnJS(haptic)() scheduleOnRN(haptic)
hitSecond.set(true) hitSecond.set(true)
} else if ( } else if (
hitSecond.get() && hitSecond.get() &&
@@ -178,7 +178,7 @@ export function GestureActionView({
!hitFirst.get() !hitFirst.get()
) { ) {
runPopAnimation() runPopAnimation()
runOnJS(haptic)() scheduleOnRN(haptic)
hitFirst.set(true) hitFirst.set(true)
} else if ( } else if (
hitFirst.get() && hitFirst.get() &&
@@ -193,15 +193,15 @@ export function GestureActionView({
'worklet' 'worklet'
if (e.translationX < 0) { if (e.translationX < 0) {
if (hitSecond.get() && actions.leftSecond) { if (hitSecond.get() && actions.leftSecond) {
runOnJS(actions.leftSecond.action)() scheduleOnRN(actions.leftSecond.action)
} else if (hitFirst.get() && actions.leftFirst) { } else if (hitFirst.get() && actions.leftFirst) {
runOnJS(actions.leftFirst.action)() scheduleOnRN(actions.leftFirst.action)
} }
} else if (e.translationX > 0) { } else if (e.translationX > 0) {
if (hitSecond.get() && actions.rightSecond) { if (hitSecond.get() && actions.rightSecond) {
runOnJS(actions.rightSecond.action)() scheduleOnRN(actions.rightSecond.action)
} else if (hitSecond.get() && actions.rightFirst) { } else if (hitSecond.get() && actions.rightFirst) {
runOnJS(actions.rightFirst.action)() scheduleOnRN(actions.rightFirst.action)
} }
} }
transX.set(() => withTiming(0, {duration: 200})) transX.set(() => withTiming(0, {duration: 200}))
@@ -7,10 +7,10 @@ import {
import Animated, { import Animated, {
Extrapolation, Extrapolation,
interpolate, interpolate,
runOnJS,
useAnimatedStyle, useAnimatedStyle,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import {GlassContainer} from 'expo-glass-effect' import {GlassContainer} from 'expo-glass-effect'
import {LinearGradient} from 'expo-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {type $Typed, type ChatBskyConvoDefs} from '@atproto/api' import {type $Typed, type ChatBskyConvoDefs} from '@atproto/api'
@@ -95,7 +95,7 @@ export function MessageComposer({
onEnd: evt => { onEnd: evt => {
'worklet' 'worklet'
if (IS_ANDROID && evt.progress === 0) { if (IS_ANDROID && evt.progress === 0) {
runOnJS(blur)() scheduleOnRN(blur)
} }
}, },
}) })
@@ -13,7 +13,6 @@ import {
KeyboardGestureArea, KeyboardGestureArea,
} from 'react-native-keyboard-controller' } from 'react-native-keyboard-controller'
import Animated, { import Animated, {
runOnJS,
type ScrollEvent, type ScrollEvent,
type SharedValue, type SharedValue,
useAnimatedStyle, useAnimatedStyle,
@@ -22,6 +21,7 @@ import Animated, {
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import { import {
type $Typed, type $Typed,
type AppBskyEmbedRecord, type AppBskyEmbedRecord,
@@ -502,7 +502,7 @@ export function MessagesList({
(e.contentOffset.y > newMessagesPill.startContentOffset + 200 || (e.contentOffset.y > newMessagesPill.startContentOffset + 200 ||
isAtBottom.get()) isAtBottom.get())
) { ) {
runOnJS(setNewMessagesPill)({ scheduleOnRN(setNewMessagesPill, {
show: false, show: false,
startContentOffset: 0, startContentOffset: 0,
}) })
@@ -4,13 +4,13 @@ import Animated, {
type AnimatedRef, type AnimatedRef,
Extrapolation, Extrapolation,
interpolate, interpolate,
runOnJS,
type SharedValue, type SharedValue,
useAnimatedProps, useAnimatedProps,
useAnimatedReaction, useAnimatedReaction,
useAnimatedStyle, useAnimatedStyle,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import {BlurView} from 'expo-blur' import {BlurView} from 'expo-blur'
import {useIsFetching} from '@tanstack/react-query' import {useIsFetching} from '@tanstack/react-query'
@@ -226,7 +226,7 @@ function useShouldAnimateSpinner({
() => scrollY.get() < -5, () => scrollY.get() < -5,
(value, prevValue) => { (value, prevValue) => {
if (value !== prevValue) { if (value !== prevValue) {
runOnJS(setIsOverscrolled)(value) scheduleOnRN(setIsOverscrolled, value)
} }
}, },
[scrollY], [scrollY],
+2 -2
View File
@@ -1,12 +1,12 @@
import {memo, useMemo, useState} from 'react' import {memo, useMemo, useState} from 'react'
import {type LayoutChangeEvent, StyleSheet, View} from 'react-native' import {type LayoutChangeEvent, StyleSheet, View} from 'react-native'
import Animated, { import Animated, {
runOnJS,
useAnimatedReaction, useAnimatedReaction,
useAnimatedStyle, useAnimatedStyle,
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import { import {
type AppBskyActorDefs, type AppBskyActorDefs,
type AppBskyLabelerDefs, type AppBskyLabelerDefs,
@@ -151,7 +151,7 @@ const MinimalHeader = memo(function MinimalHeader({
() => scrollY.get() > 100, () => scrollY.get() > 100,
(value, prev) => { (value, prev) => {
if (prev !== value) { if (prev !== value) {
runOnJS(setVisible)(value) scheduleOnRN(setVisible, value)
} }
}, },
) )
@@ -8,8 +8,6 @@ import {
import Animated, { import Animated, {
clamp, clamp,
interpolate, interpolate,
runOnJS,
runOnUI,
type SharedValue, type SharedValue,
useAnimatedReaction, useAnimatedReaction,
useAnimatedStyle, useAnimatedStyle,
@@ -20,6 +18,7 @@ import {
useSafeAreaFrame, useSafeAreaFrame,
useSafeAreaInsets, useSafeAreaInsets,
} from 'react-native-safe-area-context' } from 'react-native-safe-area-context'
import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets'
import {useEventListener} from 'expo' import {useEventListener} from 'expo'
import {type VideoPlayer} from 'expo-video' import {type VideoPlayer} from 'expo-video'
@@ -66,7 +65,7 @@ export function Scrubber({
() => Math.round(seekProgressSV.get()), () => Math.round(seekProgressSV.get()),
(progress, prevProgress) => { (progress, prevProgress) => {
if (progress !== prevProgress) { if (progress !== prevProgress) {
runOnJS(setCurrentSeekTime)(progress) scheduleOnRN(setCurrentSeekTime, progress)
} }
}, },
) )
@@ -76,11 +75,11 @@ export function Scrubber({
player?.seekBy(time) player?.seekBy(time)
setTimeout(() => { setTimeout(() => {
runOnUI(() => { scheduleOnUI(() => {
'worklet' 'worklet'
isSeekingSV.set(false) isSeekingSV.set(false)
seekingAnimationSV.set(withTiming(0, {duration: 500})) seekingAnimationSV.set(withTiming(0, {duration: 500}))
})() })
}, 50) }, 50)
}, },
[player, isSeekingSV, seekingAnimationSV], [player, isSeekingSV, seekingAnimationSV],
@@ -116,7 +115,7 @@ export function Scrubber({
// it's seek by, so offset by the current time // it's seek by, so offset by the current time
// seekBy sets isSeekingSV back to false, so no need to do that here // seekBy sets isSeekingSV back to false, so no need to do that here
runOnJS(seekBy)(newTime - currentTimeSV.get()) scheduleOnRN(seekBy, newTime - currentTimeSV.get())
}) })
}, [ }, [
scrollGesture, scrollGesture,
@@ -253,7 +252,7 @@ function PlayerListener({
if (duration !== 0) { if (duration !== 0) {
setDuration(Math.round(duration)) setDuration(Math.round(duration))
} }
runOnUI(updateTime)(evt.currentTime, duration) scheduleOnUI(updateTime, evt.currentTime, duration)
}) })
return null return null
+2 -2
View File
@@ -32,7 +32,6 @@ import Animated, {
interpolateColor, interpolateColor,
LayoutAnimationConfig, LayoutAnimationConfig,
LinearTransition, LinearTransition,
runOnUI,
scrollTo, scrollTo,
useAnimatedRef, useAnimatedRef,
useAnimatedScrollHandler, useAnimatedScrollHandler,
@@ -45,6 +44,7 @@ import Animated, {
ZoomOut, ZoomOut,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnUI} from 'react-native-worklets'
import * as FileSystem from 'expo-file-system' import * as FileSystem from 'expo-file-system'
import {type ImagePickerAsset} from 'expo-image-picker' import {type ImagePickerAsset} from 'expo-image-picker'
import { import {
@@ -2382,7 +2382,7 @@ function useScrollTracker({
const onScrollViewContentSizeChange = useCallback( const onScrollViewContentSizeChange = useCallback(
(_width: number, height: number) => { (_width: number, height: number) => {
runOnUI(onScrollViewContentSizeChangeUIThread)(height) scheduleOnUI(onScrollViewContentSizeChangeUIThread, height)
}, },
[onScrollViewContentSizeChangeUIThread], [onScrollViewContentSizeChangeUIThread],
) )
+3 -3
View File
@@ -18,12 +18,12 @@ import PagerView, {
type PageScrollStateChangedNativeEventData, type PageScrollStateChangedNativeEventData,
} from 'react-native-pager-view' } from 'react-native-pager-view'
import Animated, { import Animated, {
runOnJS,
type SharedValue, type SharedValue,
useEvent, useEvent,
useHandler, useHandler,
useSharedValue, useSharedValue,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {useSetDrawerSwipeDisabled} from '#/state/shell' import {useSetDrawerSwipeDisabled} from '#/state/shell'
@@ -125,7 +125,7 @@ export function Pager({
}, },
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) { onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
'worklet' 'worklet'
runOnJS(setIsIdle)(e.pageScrollState === 'idle') scheduleOnRN(setIsIdle, e.pageScrollState === 'idle')
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') { if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
// This is a programmatic scroll on Android. // This is a programmatic scroll on Android.
// Stay "idle" to match iOS and avoid confusing downstream code. // Stay "idle" to match iOS and avoid confusing downstream code.
@@ -137,7 +137,7 @@ export function Pager({
onPageSelected(e: PagerViewOnPageSelectedEventData) { onPageSelected(e: PagerViewOnPageSelectedEventData) {
'worklet' 'worklet'
didInit.set(true) didInit.set(true)
runOnJS(onPageSelectedJSThread)(e.position) scheduleOnRN(onPageSelectedJSThread, e.position)
}, },
}, },
[parentOnPageScrollStateChanged], [parentOnPageScrollStateChanged],
+2 -2
View File
@@ -8,13 +8,13 @@ import {
} from 'react-native' } from 'react-native'
import Animated, { import Animated, {
type AnimatedRef, type AnimatedRef,
runOnUI,
scrollTo, scrollTo,
type SharedValue, type SharedValue,
useAnimatedRef, useAnimatedRef,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {scheduleOnUI} from 'react-native-worklets'
import {ScrollProvider} from '#/lib/ScrollContext' import {ScrollProvider} from '#/lib/ScrollContext'
import { import {
@@ -181,7 +181,7 @@ export function PagerWithHeader({
) )
const onTabPressed = useCallback(() => { const onTabPressed = useCallback(() => {
runOnUI(adjustScrollForOtherPages)('dragging') scheduleOnUI(adjustScrollForOtherPages, 'dragging')
}, [adjustScrollForOtherPages]) }, [adjustScrollForOtherPages])
return ( return (
+5 -6
View File
@@ -7,8 +7,6 @@ import {
} from 'react-native' } from 'react-native'
import Animated, { import Animated, {
interpolate, interpolate,
runOnJS,
runOnUI,
scrollTo, scrollTo,
type SharedValue, type SharedValue,
useAnimatedReaction, useAnimatedReaction,
@@ -16,6 +14,7 @@ import Animated, {
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets'
import {PressableWithHover} from '#/view/com/util/PressableWithHover' import {PressableWithHover} from '#/view/com/util/PressableWithHover'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
@@ -126,7 +125,7 @@ export function TabBar({
const progress = dragProgress.get() const progress = dragProgress.get()
const offset = progressToOffset(progress) const offset = progressToOffset(progress)
// It's unclear why we need to go back to JS here. It seems iOS-specific. // It's unclear why we need to go back to JS here. It seems iOS-specific.
runOnJS(scrollToOffsetJS)(offset) scheduleOnRN(scrollToOffsetJS, offset)
} }
} }
}, },
@@ -303,7 +302,7 @@ export function TabBar({
const onPressItem = useCallback( const onPressItem = useCallback(
(index: number) => { (index: number) => {
runOnUI(onPressUIThread)(index) scheduleOnUI(onPressUIThread, index)
onSelect?.(index) onSelect?.(index)
if (index === selectedPage) { if (index === selectedPage) {
onPressSelected?.(index) onPressSelected?.(index)
@@ -409,14 +408,14 @@ function TabBarItem({
const handleLayout = useCallback( const handleLayout = useCallback(
(e: LayoutChangeEvent) => { (e: LayoutChangeEvent) => {
runOnUI(onItemLayout)(index, e.nativeEvent.layout) scheduleOnUI(onItemLayout, index, e.nativeEvent.layout)
}, },
[index, onItemLayout], [index, onItemLayout],
) )
const handleTextLayout = useCallback( const handleTextLayout = useCallback(
(e: LayoutChangeEvent) => { (e: LayoutChangeEvent) => {
runOnUI(onTextLayout)(index, e.nativeEvent.layout) scheduleOnUI(onTextLayout, index, e.nativeEvent.layout)
}, },
[index, onTextLayout], [index, onTextLayout],
) )
+5 -5
View File
@@ -2,10 +2,10 @@ import {forwardRef, memo, useDeferredValue, useMemo} from 'react'
import {RefreshControl, type ViewToken} from 'react-native' import {RefreshControl, type ViewToken} from 'react-native'
import { import {
type FlatListPropsWithLayout, type FlatListPropsWithLayout,
runOnJS,
useAnimatedScrollHandler, useAnimatedScrollHandler,
useSharedValue, useSharedValue,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {updateActiveVideoViewAsync} from '@bsky.app/video' import {updateActiveVideoViewAsync} from '@bsky.app/video'
import {useDedupe} from '#/lib/hooks/useDedupe' import {useDedupe} from '#/lib/hooks/useDedupe'
@@ -85,7 +85,7 @@ let List = forwardRef<ListMethods, ListProps>(
onBeginDragFromContext?.(e, ctx) onBeginDragFromContext?.(e, ctx)
}, },
onEndDrag(e, ctx) { onEndDrag(e, ctx) {
runOnJS(updateActiveVideoViewAsync)() scheduleOnRN(updateActiveVideoViewAsync)
onEndDragFromContext?.(e, ctx) onEndDragFromContext?.(e, ctx)
}, },
onScroll(e, ctx) { onScroll(e, ctx) {
@@ -95,18 +95,18 @@ let List = forwardRef<ListMethods, ListProps>(
if (isScrolledDown.get() !== didScrollDown) { if (isScrolledDown.get() !== didScrollDown) {
isScrolledDown.set(didScrollDown) isScrolledDown.set(didScrollDown)
if (onScrolledDownChange != null) { if (onScrolledDownChange != null) {
runOnJS(handleScrolledDownChange)(didScrollDown) scheduleOnRN(handleScrolledDownChange, didScrollDown)
} }
} }
if (IS_IOS) { if (IS_IOS) {
runOnJS(dedupe)(updateActiveVideoViewAsync) scheduleOnRN(dedupe, updateActiveVideoViewAsync)
} }
}, },
// Note: adding onMomentumBegin here makes simulator scroll // Note: adding onMomentumBegin here makes simulator scroll
// lag on Android. So either don't add it, or figure out why. // lag on Android. So either don't add it, or figure out why.
onMomentumEnd(e, ctx) { onMomentumEnd(e, ctx) {
runOnJS(updateActiveVideoViewAsync)() scheduleOnRN(updateActiveVideoViewAsync)
onMomentumEndFromContext?.(e, ctx) onMomentumEndFromContext?.(e, ctx)
}, },
}) })