Skip to content

Commit 1ed71d9

Browse files
committed
fix(scroll-shadow): create animated component on re-render
1 parent 3cdc4a2 commit 1ed71d9

File tree

4 files changed

+50
-166
lines changed

4 files changed

+50
-166
lines changed

example/src/app/(home)/_layout.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,6 @@ export default function Layout() {
111111
name="components/scroll-shadow"
112112
options={{ title: 'Scroll Shadow' }}
113113
/>
114-
<Stack.Screen
115-
name="components/scroll-shadow-test"
116-
options={{ title: 'Scroll Shadow Test' }}
117-
/>
118114
<Stack.Screen
119115
name="components/select-native-modal"
120116
options={{ title: 'Select Native Modal', presentation: 'formSheet' }}

example/src/app/(home)/components/index.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,6 @@ const components: Component[] = [
7272
title: 'Scroll Shadow',
7373
path: 'scroll-shadow',
7474
},
75-
{
76-
title: 'Scroll Shadow Test',
77-
path: 'scroll-shadow-test',
78-
},
7975
{
8076
title: 'Select',
8177
path: 'select',

example/src/app/(home)/components/scroll-shadow-test.tsx

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

src/components/scroll-shadow/scroll-shadow.tsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { cloneElement, createElement, forwardRef, isValidElement } from 'react';
1+
import {
2+
cloneElement,
3+
createElement,
4+
forwardRef,
5+
isValidElement,
6+
type ComponentType,
7+
} from 'react';
28
import { StyleSheet, View, type LayoutChangeEvent } from 'react-native';
39
import Animated, {
410
Extrapolation,
@@ -21,6 +27,48 @@ import {
2127
import { nativeStyles, scrollShadowStyles } from './scroll-shadow.styles';
2228
import type { ScrollShadowProps } from './scroll-shadow.types';
2329

30+
/**
31+
* Cache for animated components to prevent remounting on every render.
32+
* Using WeakMap ensures components are garbage collected when no longer referenced.
33+
*/
34+
const animatedComponentCache = new WeakMap<
35+
ComponentType<any>,
36+
ComponentType<any>
37+
>();
38+
39+
/**
40+
* Gets or creates a cached animated component for the given component type.
41+
* This prevents creating new component types on every render, which would cause
42+
* React to treat them as different components and trigger unmount/remount cycles.
43+
*
44+
* @param ComponentType - The original component type to create an animated version of
45+
* @returns The cached animated component type
46+
* @throws If ComponentType is not a valid component type or if Animated.createAnimatedComponent fails
47+
*/
48+
function getAnimatedComponent(
49+
ComponentType: ComponentType<any>
50+
): ComponentType<any> {
51+
// Validate that ComponentType is actually a function/component
52+
if (typeof ComponentType !== 'function') {
53+
throw new Error(
54+
'ScrollShadow: children.type must be a valid React component type'
55+
);
56+
}
57+
58+
let cached = animatedComponentCache.get(ComponentType);
59+
if (!cached) {
60+
try {
61+
cached = Animated.createAnimatedComponent(ComponentType);
62+
animatedComponentCache.set(ComponentType, cached);
63+
} catch (error) {
64+
throw new Error(
65+
`ScrollShadow: Failed to create animated component: ${error instanceof Error ? error.message : String(error)}`
66+
);
67+
}
68+
}
69+
return cached;
70+
}
71+
2472
const ScrollShadowRoot = forwardRef<View, ScrollShadowProps>((props, ref) => {
2573
const {
2674
children,
@@ -144,7 +192,7 @@ const ScrollShadowRoot = forwardRef<View, ScrollShadowProps>((props, ref) => {
144192
scrollEventThrottle,
145193
onScroll,
146194
})
147-
: createElement(Animated.createAnimatedComponent(children.type as any), {
195+
: createElement(getAnimatedComponent(children.type as any), {
148196
...(children as any).props,
149197
onContentSizeChange,
150198
onLayout,

0 commit comments

Comments
 (0)