Skip to content

Commit 0db8e57

Browse files
authored
fix: fix refs handling for custom scrollables (#20)
1 parent 9373580 commit 0db8e57

17 files changed

Lines changed: 792 additions & 55 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Full documentation lives here:
3030
- Scroll-driven animated headers
3131
- Shared header state across tabs, pagers, and multiple scrollables
3232
- Navigation-rendered headers in Expo Router or React Navigation
33-
- Custom scrollables via `createHeaderMotionScrollable()`
33+
- Custom scrollables via `createHeaderMotionScrollable()` and `ScrollablePresets`
3434
- Optional header panning
3535

3636
## What it is not

docs/docs/api/create-header-motion-scrollable.md

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Factory function for creating reusable Header Motion-aware wrappers around custo
99

1010
This is the recommended way to integrate third-party scrollables like FlashList or LegendList.
1111

12+
For common integrations, the library also exports `ScrollablePresets` so you do not need to remember the recommended option combinations yourself.
13+
1214
## Signature
1315

1416
```tsx
@@ -20,38 +22,55 @@ function createHeaderMotionScrollable(
2022

2123
## Options
2224

23-
| Option | Type | Default | Description |
24-
| ---------------------- | --------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
25-
| `displayName` | `string` | Auto-generated | Component name shown in React DevTools. |
26-
| `isComponentAnimated` | `boolean` | `false` | Set to `true` when the component is already animated (skips `Animated.createAnimatedComponent`). |
27-
| `contentContainerMode` | `'children' \| 'renderScrollComponent'` | `'renderScrollComponent'` | How Header Motion injects content offsetting. Use `'children'` for ScrollView-like, `'renderScrollComponent'` for FlatList-like. |
25+
| Option | Type | Default | Description |
26+
| ---------------------- | --------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
27+
| `displayName` | `string` | Auto-generated | Component name shown in React DevTools. |
28+
| `isComponentAnimated` | `boolean` | `false` | Set to `true` when the component is already animated (skips `Animated.createAnimatedComponent`). |
29+
| `contentContainerMode` | `'children' \| 'renderScrollComponent'` | `'renderScrollComponent'` | How Header Motion injects content offsetting. Use `'children'` for ScrollView-like, `'renderScrollComponent'` for FlatList-like. |
30+
| `managedRefTarget` | `'outer' \| 'inner'` | `'outer'` | Which ref Header Motion uses for imperative sync. Only applies in `renderScrollComponent` mode. Use `'inner'` when the wrapped list's outer ref is not the actual scroll view. |
2831

2932
## Examples
3033

31-
### FlashList
34+
### FlashList preset
3235

3336
```tsx
3437
import { FlashList } from '@shopify/flash-list';
35-
import { createHeaderMotionScrollable } from 'react-native-header-motion';
38+
import {
39+
createHeaderMotionScrollable,
40+
ScrollablePresets,
41+
} from 'react-native-header-motion';
3642

37-
const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
38-
displayName: 'HeaderMotionFlashList',
39-
});
43+
const HeaderMotionFlashList = createHeaderMotionScrollable(
44+
FlashList,
45+
ScrollablePresets.FlashList
46+
);
4047
```
4148

42-
### LegendList
49+
### LegendList preset
4350

4451
```tsx
4552
import { AnimatedLegendList } from '@legendapp/list/reanimated';
46-
import { createHeaderMotionScrollable } from 'react-native-header-motion';
53+
import {
54+
createHeaderMotionScrollable,
55+
ScrollablePresets,
56+
} from 'react-native-header-motion';
4757

4858
const HeaderMotionLegendList = createHeaderMotionScrollable(
4959
AnimatedLegendList,
50-
{
51-
displayName: 'HeaderMotionLegendList',
52-
isComponentAnimated: true,
53-
}
60+
ScrollablePresets.AnimatedLegendList
5461
);
5562
```
5663

64+
### Manual options
65+
66+
```tsx
67+
const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
68+
displayName: 'HeaderMotionFlashList',
69+
contentContainerMode: 'renderScrollComponent',
70+
managedRefTarget: 'inner',
71+
});
72+
```
73+
74+
Use `managedRefTarget: 'inner'` when the wrapped component exposes a controller ref on the outside but owns the actual native scroll view internally. FlashList and LegendList both need this. React Native `FlatList` does not, so the default `'outer'` behavior remains correct there.
75+
5776
The returned component accepts all the original component's props plus Header Motion-specific props (`scrollId`, `headerOffsetStrategy`, `ensureScrollableContentMinHeight`, `animatedRef`).

docs/docs/guides/custom-scrollables.md

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@ title: Custom scrollables
77

88
Header Motion ships with `HeaderMotion.ScrollView` and `HeaderMotion.FlatList`, but you can integrate **any** scrollable component via the `createHeaderMotionScrollable()` factory. The resulting component works just like the built-in ones — it accepts all the original props plus Header Motion-specific ones like `scrollId` and `headerOffsetStrategy`.
99

10+
For popular third-party lists, the library also exports `ScrollablePresets` with the recommended option combinations.
11+
1012
## FlashList
1113

1214
```tsx
1315
import { FlashList } from '@shopify/flash-list';
14-
import { createHeaderMotionScrollable } from 'react-native-header-motion';
16+
import {
17+
createHeaderMotionScrollable,
18+
ScrollablePresets,
19+
} from 'react-native-header-motion';
1520

16-
const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
17-
displayName: 'HeaderMotionFlashList',
18-
});
21+
const HeaderMotionFlashList = createHeaderMotionScrollable(
22+
FlashList,
23+
ScrollablePresets.FlashList
24+
);
1925
```
2026

2127
Use it anywhere you'd use a regular `FlashList`:
@@ -35,24 +41,48 @@ LegendList exports a pre-animated variant, `AnimatedLegendList`, which is what y
3541

3642
```tsx
3743
import { AnimatedLegendList } from '@legendapp/list/reanimated';
38-
import { createHeaderMotionScrollable } from 'react-native-header-motion';
44+
import {
45+
createHeaderMotionScrollable,
46+
ScrollablePresets,
47+
} from 'react-native-header-motion';
3948

4049
const HeaderMotionLegendList = createHeaderMotionScrollable(
4150
AnimatedLegendList,
42-
{
43-
displayName: 'HeaderMotionLegendList',
44-
isComponentAnimated: true,
45-
}
51+
ScrollablePresets.AnimatedLegendList
4652
);
4753
```
4854

55+
## Presets vs manual options
56+
57+
`ScrollablePresets` is the easiest path when you are integrating supported third-party lists:
58+
59+
```tsx
60+
import { ScrollablePresets } from 'react-native-header-motion';
61+
62+
ScrollablePresets.FlashList;
63+
ScrollablePresets.AnimatedLegendList;
64+
```
65+
66+
If you prefer to configure things manually, these are the equivalent options:
67+
68+
```tsx
69+
const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
70+
displayName: 'HeaderMotionFlashList',
71+
contentContainerMode: 'renderScrollComponent',
72+
managedRefTarget: 'inner',
73+
});
74+
```
75+
76+
Use `managedRefTarget: 'inner'` when a third-party list exposes a controller ref on the outside but keeps the real native scroll view one layer deeper. FlashList and LegendList both fall into that category. React Native `FlatList` does not, so it should keep the default `'outer'`.
77+
4978
## Factory options
5079

5180
| Option | Type | Default | Description |
5281
| ---------------------- | --------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
5382
| `displayName` | `string` | Auto-derived from the component name | Sets the React `displayName` for dev tools. |
5483
| `isComponentAnimated` | `boolean` | `false` | When `true`, the factory skips `Animated.createAnimatedComponent()`. Set this when the component is already animated. |
5584
| `contentContainerMode` | `'children' \| 'renderScrollComponent'` | `'renderScrollComponent'` | `'children'` wraps children in an inner `Animated.View` (ScrollView-like). `'renderScrollComponent'` injects a custom scroll component that wraps the content (FlatList-like). |
85+
| `managedRefTarget` | `'outer' \| 'inner'` | `'outer'` | Controls which ref Header Motion uses for imperative sync. Only applies in `renderScrollComponent` mode. Use `'inner'` for list abstractions like FlashList and LegendList. |
5686

5787
## What's next?
5888

docs/docs/guides/default-scrollables.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ You can also pass standard scroll event handlers like `onScroll`, `onScrollBegin
6666

6767
## Custom scrollables
6868

69-
For third-party scrollable components like [FlashList](https://shopify.github.io/flash-list/) or [LegendList](https://github.com/LegendApp/legend-list), use the `createHeaderMotionScrollable()` factory. For more information, check the [Custom scrollables](./custom-scrollables) guide.
69+
For third-party scrollable components like [FlashList](https://shopify.github.io/flash-list/) or [LegendList](https://github.com/LegendApp/legend-list), use the `createHeaderMotionScrollable()` factory. For those two common integrations, you can also start from the exported `ScrollablePresets`. For more information, check the [Custom scrollables](./custom-scrollables) guide.
7070

7171
For even more control, you can manage the scroll integration yourself using `HeaderMotion.ScrollManager` or the `useScrollManager()` hook. See [Using ScrollManager](./using-scroll-manager) for details.
7272

docs/docs/other/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Header panning is built on Gesture Handler's pan gesture. Even if you don't use
2222

2323
## Can I use this with FlashList / LegendList / other custom scrollables?
2424

25-
Yes. Use `createHeaderMotionScrollable()` to wrap any scrollable component. See the [Custom scrollables](../guides/custom-scrollables) guide.
25+
Yes. Use `createHeaderMotionScrollable()` to wrap any scrollable component. For FlashList and LegendList, you can also use the built-in `ScrollablePresets` export so the recommended factory options are already filled in. See the [Custom scrollables](../guides/custom-scrollables) guide.
2626

2727
## Does pull to refresh work?
2828

example/src/app/flashlist-pager.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '@/components';
77
import HeaderMotion, {
88
createHeaderMotionScrollable,
9+
ScrollablePresets,
910
useActiveScrollId,
1011
useMotionProgress,
1112
} from 'react-native-header-motion';
@@ -23,9 +24,10 @@ import Animated, {
2324
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2425
import { FlashList } from '@shopify/flash-list';
2526

26-
const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
27-
displayName: 'HeaderMotionFlashList',
28-
});
27+
const HeaderMotionFlashList = createHeaderMotionScrollable(
28+
FlashList,
29+
ScrollablePresets.FlashList
30+
);
2931

3032
const indexToKey = new Map([
3133
[0, 'A'],

example/src/app/flashlist.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ContentCard, ShowcaseCollapsibleHeader } from '@/components';
22
import { FlashList } from '@shopify/flash-list';
33
import HeaderMotion, {
44
createHeaderMotionScrollable,
5+
ScrollablePresets,
56
} from 'react-native-header-motion';
67
import { Stack } from 'expo-router';
78

@@ -10,9 +11,10 @@ type ListRow = {
1011
label: string;
1112
};
1213

13-
const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
14-
displayName: 'HeaderMotionFlashList',
15-
});
14+
const HeaderMotionFlashList = createHeaderMotionScrollable(
15+
FlashList,
16+
ScrollablePresets.FlashList
17+
);
1618

1719
export default function Screen() {
1820
return (

example/src/app/index.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ const SECTIONS: ShowcaseSection[] = [
9696
href: '/scroll-to-button',
9797
icon: '🎯',
9898
},
99+
{
100+
title: 'Scroll To Button (external ref) | FlatList',
101+
href: '/scroll-to-button',
102+
icon: '📋🎯',
103+
},
104+
{
105+
title: 'Scroll To Button (external ref) | FlashList',
106+
href: '/scroll-to-button',
107+
icon: '⚡️🎯',
108+
},
109+
{
110+
title: 'Scroll To Button (external ref) | LegendList',
111+
href: '/scroll-to-button',
112+
icon: '🧾🎯',
113+
},
99114
],
100115
},
101116
{

example/src/app/legendlist-pager.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '@/components';
77
import HeaderMotion, {
88
createHeaderMotionScrollable,
9+
ScrollablePresets,
910
useActiveScrollId,
1011
useMotionProgress,
1112
} from 'react-native-header-motion';
@@ -25,10 +26,7 @@ import { AnimatedLegendList } from '@legendapp/list/reanimated';
2526

2627
const HeaderMotionLegendList = createHeaderMotionScrollable(
2728
AnimatedLegendList,
28-
{
29-
displayName: 'HeaderMotionLegendList',
30-
isComponentAnimated: true,
31-
}
29+
ScrollablePresets.AnimatedLegendList
3230
);
3331

3432
const indexToKey = new Map([

example/src/app/legendlist.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ContentCard, ShowcaseCollapsibleHeader } from '@/components';
22
import HeaderMotion, {
33
createHeaderMotionScrollable,
4+
ScrollablePresets,
45
} from 'react-native-header-motion';
56
import { Stack } from 'expo-router';
67

@@ -13,10 +14,7 @@ type ListRow = {
1314

1415
const HeaderMotionLegendList = createHeaderMotionScrollable(
1516
AnimatedLegendList,
16-
{
17-
displayName: 'HeaderMotionLegendList',
18-
isComponentAnimated: true,
19-
}
17+
ScrollablePresets.AnimatedLegendList
2018
);
2119

2220
export default function Screen() {

0 commit comments

Comments
 (0)