feat: expose useScrollEventsHandlersDefault
Please provide enough information so that others can review your pull request:
Motivation
Exposing useScrollEventsHandlersDefault will make it way easier to modify the behaviour of react-native-bottom-sheet scroll handlers, when you need to perform additional actions, after the default ones are done, without fully rewriting them from scratch
hi @schamberg97 👋,
could you provide a use-case of using useScrollEventsHandlersDefault ? the hook useScrollEventsHandlersDefault does not allow any modification. You would need to re-write the scroll events just like the exmaple here: https://github.com/gorhom/react-native-bottom-sheet/blob/master/example/src/screens/advanced/customGestureHandling/useCustomScrollEventsHandlers.tsx
one of the reasons non of these hooks are exported , is that the api still not announce and might change in next non-major releases.
We currently use some dirty hacks (partially inspired by the example) in a project to get the current scroll Y position, which rely on useScrollEventsHandlersDefault being exposed. Basically, we build a custom hook that encapsulates the scroll functions from useScrollEventsHandlersDefault with our own additional logic. This ensures:
- That we can run our own function that consumes
NativeScrollEvent, which we need to get the current Y scroll position - That we can afterwards run
handleOnScrollfromuseScrollEventsHandlersDefault, without otherwise touching internal logic
(Take note that the order of execution is not crucial to us, our custom logic could as easily execute after react-native-bottom-sheet functions)
Unfortunately, we have to do that to simply get the Y position in the scrollable FlatList, which is, frankly, overkill. Perhaps we are missing or misunderstand something from the documentation and there is indeed a better way to do this, other than to build custom hooks?
Here's the code sample
import { useWorkletCallback, runOnJS } from 'react-native-reanimated';
import {ScrollEventsHandlersHookType, useScrollEventsHandlersDefault} from '@gorhom/bottom-sheet';
import type { NativeScrollEvent } from 'react-native';
type FnNativeEvent = (e: NativeScrollEvent) => void;
type BuildCustomScrollHooksInput = {
onScroll?: FnNativeEvent;
[key: string]: FnNativeEvent
};
export function buildScrollEventsHandlersHook(
customHandlers: BuildCustomScrollHooksInput,
// we can pass what would've been normal scroll handlers, but in practice we only use onScroll
): ScrollEventsHandlersHookType {
const useHandlers = (
workletCallback: ReturnType<typeof useWorkletCallback>,
jsHandler?: FnNativeEvent,
) =>
useWorkletCallback(
(e, c) => {
workletCallback(e, c);
if (jsHandler) runOnJS(jsHandler)(e);
},
[jsHandler, workletCallback],
);
const useScrollEventsHandlers: ScrollEventsHandlersHookType = (
ref,
contentOffsetY,
) => {
const defaultHandlers = useScrollEventsHandlersDefault(ref, contentOffsetY);
return {
handleOnScroll: useHandlers(
defaultHandlers.handleOnScroll,
customHandlers.onScroll, // onScroll gets NativeScrollEvent that we need it to consume
),
handleOnSomething: useHandlers(
defaultHandlers.handleOnSomething,
customHandlers.onSomething
)
// other handlers, if onScroll is insufficient for the use case
};
};
return useScrollEventsHandlers
}
const usePrebuiltScrollEventsHandlers = buildScrollEventsHandlersHook({
onScroll: (e: NativeScrollEvent) => console.log(e) // in practice, we only use this injected function to monitor scroll position for various purposes
}) // we can then pass this prebuilt Hook as scrollEventsHandlersHook prop
@gorhom any ideas?
Another hook that provides scrollY for external animations:
import {
SharedValue,
useSharedValue,
useWorkletCallback,
} from 'react-native-reanimated';
import {
ScrollEventHandlerCallbackType,
ScrollEventsHandlersHookType,
useScrollEventsHandlersDefault,
} from '@gorhom/bottom-sheet';
import { ScrollEventContextType } from '@gorhom/bottom-sheet/lib/typescript/hooks/useScrollEventsHandlersDefault';
export const useScrollEventsHandlersCustom: () => {
useDefaultHook: ScrollEventsHandlersHookType;
scrollY: SharedValue<number>;
} = () => {
const scrollY = useSharedValue(0);
return {
useDefaultHook: (scrollableRef, scrollableContentOffsetY) => {
const { handleOnScroll, ...rest } = useScrollEventsHandlersDefault(
scrollableRef,
scrollableContentOffsetY,
);
const handleOnScrollCustom: ScrollEventHandlerCallbackType<ScrollEventContextType> =
useWorkletCallback(
(event, ctx) => {
scrollY.value = event.contentOffset.y;
handleOnScroll?.(event, ctx);
},
[handleOnScroll, scrollY],
);
return {
...rest,
handleOnScroll: handleOnScrollCustom,
};
},
scrollY,
};
};
Usage:
const { useDefaultHook, scrollY } = useScrollEventsHandlersCustom();
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
scale: interpolate(
scrollY.value,
[-100, 0],
[1.2, 1],
Extrapolate.CLAMP,
),
},
],
};
});
<BottomSheetScrollView scrollEventsHandlersHook={useDefaultHook}>
<SomeAnimatedView style={animatedStyle} />
</BottomSheetScrollView>
This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 10 days.
Not stale
@hirbod it's possible that the issue is partially resolved in 4.4.0 (at least for my company's use case), gonna need to test it. I am not going to close this issue yet, since it may not necessarily be what you need (please see https://github.com/gorhom/react-native-bottom-sheet/compare/v4.3.2...v4.4.0)
Yes, it should be resolved
This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 10 days.
Not stale
This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 10 days.
For anyone who needs to find a way of subscribing to the bottom sheet animated position, can try the following:
const animatedPosition = useSharedValue(0);
return <BottomSheet animatedPosition={animatedPosition} {...props} />
This value will be updated along with the user gesture events, similar to the user scroll event, i.e: using useAnimatedScrollHandler, The difference here is that we are not using any hooks to set the value, but directly giving the shared value. And it will be handled internally. While it is not a great DX, (I would prefer the hook) it did the job for me, since I just needed to make use of animated position.