diff --git a/example/src/Shared/QuickStartDemo.tsx b/example/src/Shared/QuickStartDemo.tsx
index d2fca6ac..2e844851 100644
--- a/example/src/Shared/QuickStartDemo.tsx
+++ b/example/src/Shared/QuickStartDemo.tsx
@@ -4,6 +4,9 @@ import { Tabs } from 'react-native-collapsible-tab-view'
const HEADER_HEIGHT = 250
+const DATA = [0, 1, 2, 3, 4]
+const identity = (v: unknown): string => v + ''
+
const Header = () => {
return
}
@@ -22,9 +25,9 @@ const Example: React.FC = () => {
>
v + ''}
+ keyExtractor={identity}
/>
diff --git a/src/Container.tsx b/src/Container.tsx
index fffe08ad..de37285d 100644
--- a/src/Container.tsx
+++ b/src/Container.tsx
@@ -93,21 +93,22 @@ export const Container = React.memo(
const windowWidth = useWindowDimensions().width
const firstRender = React.useRef(true)
- const [containerHeight, setContainerHeight] = React.useState<
- number | undefined
- >(undefined)
- const [tabBarHeight, setTabBarHeight] = React.useState<
- number | undefined
- >(initialTabBarHeight)
- const [headerHeight, setHeaderHeight] = React.useState<
- number | undefined
- >(!renderHeader ? 0 : initialHeaderHeight)
-
- const contentInset = React.useMemo(
- () => (IS_IOS ? (headerHeight || 0) + (tabBarHeight || 0) : 0),
- [headerHeight, tabBarHeight]
+ const containerHeight = useSharedValue(undefined)
+
+ const tabBarHeight = useSharedValue(
+ initialTabBarHeight
+ )
+
+ const headerHeight = useSharedValue(
+ !renderHeader ? 0 : initialHeaderHeight
)
+ const contentInset = useDerivedValue(() => {
+ return IS_IOS
+ ? (headerHeight.value || 0) + (tabBarHeight.value || 0)
+ : 0
+ })
+
const isSwiping = useSharedValue(false)
const isSnapping: ContextType['isSnapping'] = useSharedValue(false)
const snappingTo: ContextType['snappingTo'] = useSharedValue(0)
@@ -156,7 +157,9 @@ export const Container = React.memo(
}, [tabNames])
const calculateNextOffset = useSharedValue(index.value)
const headerScrollDistance: ContextType['headerScrollDistance'] = useDerivedValue(() => {
- return headerHeight !== undefined ? headerHeight - minHeaderHeight : 0
+ return headerHeight.value !== undefined
+ ? headerHeight.value - minHeaderHeight
+ : 0
}, [headerHeight, minHeaderHeight])
const getItemLayout = React.useCallback(
@@ -218,7 +221,7 @@ export const Container = React.memo(
scrollToImpl(
refMap[name],
0,
- scrollY.value[index.value] - contentInset,
+ scrollY.value[index.value] - contentInset.value,
false
)
})
@@ -334,8 +337,8 @@ export const Container = React.memo(
const getHeaderHeight = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
- if (headerHeight !== height) {
- setHeaderHeight(height)
+ if (headerHeight.value !== height) {
+ headerHeight.value = height
}
},
[headerHeight]
@@ -344,7 +347,7 @@ export const Container = React.memo(
const getTabBarHeight = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
- if (tabBarHeight !== height) setTabBarHeight(height)
+ if (tabBarHeight.value !== height) tabBarHeight.value = height
},
[tabBarHeight]
)
@@ -352,7 +355,7 @@ export const Container = React.memo(
const onLayout = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
- if (containerHeight !== height) setContainerHeight(height)
+ if (containerHeight.value !== height) containerHeight.value = height
},
[containerHeight]
)
@@ -393,7 +396,7 @@ export const Container = React.memo(
runOnUI(scrollToImpl)(
ref,
0,
- headerScrollDistance.value - contentInset,
+ headerScrollDistance.value - contentInset.value,
true
)
} else {
@@ -442,8 +445,8 @@ export const Container = React.memo(
>>(
+ (props, passRef) => {
+ return (
+
+ )
+ }
+ )
+)
+
function FlatListImpl(
{
contentContainerStyle,
@@ -26,15 +44,14 @@ function FlatListImpl(
const name = useTabNameContext()
const { setRef, contentInset, scrollYCurrent } = useTabsContext()
const ref = useSharedAnimatedRef>(passRef)
- const [canBindScrollEvent, setCanBindScrollEvent] = React.useState(false)
+ const { scrollHandler, enable } = useScrollHandlerY(name)
useAfterMountEffect(() => {
// we enable the scroll event after mounting
// otherwise we get an `onScroll` call with the initial scroll position which can break things
- setCanBindScrollEvent(true)
+ enable(true)
})
- const scrollHandler = useScrollHandlerY(name, { enabled: canBindScrollEvent })
const {
style: _style,
contentContainerStyle: _contentContainerStyle,
@@ -50,35 +67,57 @@ function FlatListImpl(
})
const scrollContentSizeChangeHandlers = useChainCallback(
- scrollContentSizeChange,
- onContentSizeChange
+ React.useMemo(() => [scrollContentSizeChange, onContentSizeChange], [
+ onContentSizeChange,
+ scrollContentSizeChange,
+ ])
+ )
+
+ const memoRefreshControl = React.useMemo(
+ () =>
+ refreshControl &&
+ React.cloneElement(refreshControl, {
+ progressViewOffset,
+ ...refreshControl.props,
+ }),
+ [progressViewOffset, refreshControl]
+ )
+ const memoContentOffset = React.useMemo(
+ () => ({
+ y: IS_IOS ? -contentInset.value + scrollYCurrent.value : 0,
+ x: 0,
+ }),
+ [contentInset.value, scrollYCurrent.value]
+ )
+ const memoContentInset = React.useMemo(() => ({ top: contentInset.value }), [
+ contentInset.value,
+ ])
+ const memoContentContainerStyle = React.useMemo(
+ () => [
+ _contentContainerStyle,
+ // TODO: investigate types
+ contentContainerStyle as any,
+ ],
+ [_contentContainerStyle, contentContainerStyle]
)
+ const memoStyle = React.useMemo(() => [_style, style], [_style, style])
return (
-
)
}
diff --git a/src/MaterialTabBar/TabBar.tsx b/src/MaterialTabBar/TabBar.tsx
index d2ad13cd..f775df90 100644
--- a/src/MaterialTabBar/TabBar.tsx
+++ b/src/MaterialTabBar/TabBar.tsx
@@ -61,9 +61,8 @@ const MaterialTabBar = ({
const tabBarRef = useAnimatedRef()
const windowWidth = useWindowDimensions().width
const isFirstRender = React.useRef(true)
- const [itemsLayoutGathering, setItemsLayoutGathering] = React.useState(
- new Map()
- )
+ const itemLayoutGathering = React.useRef(new Map())
+
const tabsOffset = useSharedValue(0)
const isScrolling = useSharedValue(false)
@@ -97,30 +96,26 @@ const MaterialTabBar = ({
if (scrollEnabled) {
if (!event.nativeEvent?.layout) return
const { width, x } = event.nativeEvent.layout
- setItemsLayoutGathering((itemsLayoutGathering) => {
- const update = new Map(itemsLayoutGathering)
- return update.set(name, {
- width,
- x,
- })
+
+ itemLayoutGathering.current.set(name, {
+ width,
+ x,
})
+
+ // pick out the layouts for the tabs we know about (in case they changed dynamically)
+ const layout = Array.from(itemLayoutGathering.current.entries())
+ .filter(([tabName]) => tabNames.includes(tabName))
+ .map(([, layout]) => layout)
+ .sort((a, b) => a.x - b.x)
+
+ if (layout.length === tabNames.length) {
+ setItemsLayout(layout)
+ }
}
},
- [scrollEnabled]
+ [scrollEnabled, tabNames]
)
- React.useEffect(() => {
- // pick out the layouts for the tabs we know about (in case they changed dynamically)
- const layout = Array.from(itemsLayoutGathering.entries())
- .filter(([tabName]) => tabNames.includes(tabName))
- .map(([, layout]) => layout)
- .sort((a, b) => a.x - b.x)
-
- if (layout.length === tabNames.length) {
- setItemsLayout(layout)
- }
- }, [itemsLayoutGathering, tabNames])
-
const cancelNextScrollSync = useSharedValue(index.value)
const onScroll = useAnimatedScrollHandler(
diff --git a/src/ScrollView.tsx b/src/ScrollView.tsx
index b995e6cb..bc230fae 100644
--- a/src/ScrollView.tsx
+++ b/src/ScrollView.tsx
@@ -14,6 +14,24 @@ import {
useUpdateScrollViewContentSize,
} from './hooks'
+/**
+ * Used as a memo to prevent rerendering too often when the context changes.
+ * See: https://github.com/facebook/react/issues/15156#issuecomment-474590693
+ */
+const ScrollViewMemo = React.memo(
+ React.forwardRef>(
+ (props, passRef) => {
+ return (
+
+ )
+ }
+ )
+)
+
/**
* Use like a regular ScrollView.
*/
@@ -40,16 +58,11 @@ export const ScrollView = React.forwardRef<
contentContainerStyle: _contentContainerStyle,
progressViewOffset,
} = useCollapsibleStyle()
- const [canBindScrollEvent, setCanBindScrollEvent] = React.useState(false)
-
+ const { scrollHandler, enable } = useScrollHandlerY(name)
useAfterMountEffect(() => {
// we enable the scroll event after mounting
// otherwise we get an `onScroll` call with the initial scroll position which can break things
- setCanBindScrollEvent(true)
- })
-
- const scrollHandler = useScrollHandlerY(name, {
- enabled: canBindScrollEvent,
+ enable(true)
})
React.useEffect(() => {
@@ -61,41 +74,59 @@ export const ScrollView = React.forwardRef<
})
const scrollContentSizeChangeHandlers = useChainCallback(
- scrollContentSizeChange,
- onContentSizeChange
+ React.useMemo(() => [scrollContentSizeChange, onContentSizeChange], [
+ onContentSizeChange,
+ scrollContentSizeChange,
+ ])
+ )
+
+ const memoRefreshControl = React.useMemo(
+ () =>
+ refreshControl &&
+ React.cloneElement(refreshControl, {
+ progressViewOffset,
+ ...refreshControl.props,
+ }),
+ [progressViewOffset, refreshControl]
+ )
+ const memoContentOffset = React.useMemo(
+ () => ({
+ y: IS_IOS ? -contentInset.value + scrollYCurrent.value : 0,
+ x: 0,
+ }),
+ [contentInset.value, scrollYCurrent.value]
+ )
+ const memoContentInset = React.useMemo(
+ () => ({ top: contentInset.value }),
+ [contentInset.value]
+ )
+ const memoContentContainerStyle = React.useMemo(
+ () => [
+ _contentContainerStyle,
+ // TODO: investigate types
+ contentContainerStyle as any,
+ ],
+ [_contentContainerStyle, contentContainerStyle]
)
+ const memoStyle = React.useMemo(() => [_style, style], [_style, style])
return (
-
{children}
-
+
)
}
)
diff --git a/src/hooks.tsx b/src/hooks.tsx
index 108419f2..faaad363 100644
--- a/src/hooks.tsx
+++ b/src/hooks.tsx
@@ -127,29 +127,37 @@ export function useTabNameContext(): TabName {
export function useCollapsibleStyle(): CollapsibleStyle {
const { headerHeight, tabBarHeight, containerHeight } = useTabsContext()
const windowWidth = useWindowDimensions().width
-
- return {
- style: { width: windowWidth },
- contentContainerStyle: {
- minHeight: IS_IOS
- ? (containerHeight || 0) - tabBarHeight
- : (containerHeight || 0) + headerHeight,
- paddingTop: IS_IOS ? 0 : headerHeight + tabBarHeight,
- },
- progressViewOffset: headerHeight + tabBarHeight,
- }
+ const [containerHeightVal, tabBarHeightVal, headerHeightVal] = [
+ useConvertAnimatedToValue(containerHeight),
+ useConvertAnimatedToValue(tabBarHeight),
+ useConvertAnimatedToValue(headerHeight),
+ ]
+ return useMemo(
+ () => ({
+ style: { width: windowWidth },
+ contentContainerStyle: {
+ minHeight: IS_IOS
+ ? (containerHeightVal || 0) - (tabBarHeightVal || 0)
+ : (containerHeightVal || 0) + (headerHeightVal || 0),
+ paddingTop: IS_IOS
+ ? 0
+ : (headerHeightVal || 0) + (tabBarHeightVal || 0),
+ },
+ progressViewOffset: (headerHeightVal || 0) + (tabBarHeightVal || 0),
+ }),
+ [containerHeightVal, headerHeightVal, tabBarHeightVal, windowWidth]
+ )
}
export function useUpdateScrollViewContentSize({ name }: { name: TabName }) {
const { tabNames, contentHeights } = useTabsContext()
-
const setContentHeights = useCallback(
(name: TabName, height: number) => {
const tabIndex = tabNames.value.indexOf(name)
contentHeights.value[tabIndex] = height
contentHeights.value = [...contentHeights.value]
},
- [contentHeights, tabNames.value]
+ [contentHeights, tabNames]
)
const scrollContentSizeChange = useCallback(
@@ -168,7 +176,7 @@ export function useUpdateScrollViewContentSize({ name }: { name: TabName }) {
* @param fns array of functions to call
* @returns a function that once called will call all passed functions
*/
-export function useChainCallback(...fns: (Function | undefined)[]) {
+export function useChainCallback(fns: (Function | undefined)[]) {
const callAll = useCallback(
(...args: unknown[]) => {
fns.forEach((fn) => {
@@ -196,7 +204,7 @@ export function useScroller() {
'worklet'
if (!ref) return
// console.log(`${_debugKey}, y: ${y}, y adjusted: ${y - contentInset}`)
- scrollToImpl(ref, x, y - contentInset, animated)
+ scrollToImpl(ref, x, y - contentInset.value, animated)
},
[contentInset]
)
@@ -204,10 +212,7 @@ export function useScroller() {
return scroller
}
-export const useScrollHandlerY = (
- name: TabName,
- { enabled }: { enabled: boolean }
-) => {
+export const useScrollHandlerY = (name: TabName) => {
const {
accDiffClamp,
focusedTab,
@@ -232,6 +237,15 @@ export const useScrollHandlerY = (
contentHeights,
} = useTabsContext()
+ const enabled = useSharedValue(false)
+
+ const enable = useCallback(
+ (toggle: boolean) => {
+ enabled.value = toggle
+ },
+ [enabled]
+ )
+
/**
* Helper value to track if user is dragging on iOS, because iOS calls
* onMomentumEnd only after a vigorous swipe. If the user has finished the
@@ -249,7 +263,7 @@ export const useScrollHandlerY = (
const onMomentumEnd = () => {
'worklet'
- if (!enabled) return
+ if (!enabled.value) return
if (typeof snapThreshold === 'number') {
if (revealHeaderOnScroll) {
@@ -328,15 +342,17 @@ export const useScrollHandlerY = (
const scrollHandler = useAnimatedScrollHandler(
{
onScroll: (event) => {
- if (!enabled) return
+ if (!enabled.value) return
if (focusedTab.value === name) {
if (IS_IOS) {
let { y } = event.contentOffset
// normalize the value so it starts at 0
- y = y + contentInset
+ y = y + contentInset.value
const clampMax =
- contentHeight.value - (containerHeight || 0) + contentInset
+ contentHeight.value -
+ (containerHeight.value || 0) +
+ contentInset.value
// make sure the y value is clamped to the scrollable size (clamps overscrolling)
scrollYCurrent.value = interpolate(
y,
@@ -381,7 +397,7 @@ export const useScrollHandlerY = (
}
},
onBeginDrag: () => {
- if (!enabled) return
+ if (!enabled.value) return
// ensure the header stops snapping
cancelAnimation(accDiffClamp)
@@ -393,7 +409,7 @@ export const useScrollHandlerY = (
if (IS_IOS) cancelAnimation(afterDrag)
},
onEndDrag: () => {
- if (!enabled) return
+ if (!enabled.value) return
isGliding.value = true
@@ -414,7 +430,7 @@ export const useScrollHandlerY = (
}
},
onMomentumBegin: () => {
- if (!enabled) return
+ if (!enabled.value) return
if (IS_IOS) {
cancelAnimation(afterDrag)
@@ -438,7 +454,10 @@ export const useScrollHandlerY = (
useAnimatedReaction(
() => {
return (
- !isSnapping.value && !isScrolling.value && !isGliding.value && enabled
+ !isSnapping.value &&
+ !isScrolling.value &&
+ !isGliding.value &&
+ enabled.value
)
},
(sync) => {
@@ -462,7 +481,7 @@ export const useScrollHandlerY = (
if (focusedIsOnTop) {
nextPosition = snappingTo.value
} else if (currIsOnTop) {
- nextPosition = headerHeight
+ nextPosition = headerHeight.value || 0
}
} else if (currIsOnTop || focusedIsOnTop) {
nextPosition = Math.min(focusedScrollY, headerScrollDistance.value)
@@ -478,7 +497,7 @@ export const useScrollHandlerY = (
[revealHeaderOnScroll, refMap, snapThreshold, tabIndex, enabled, scrollTo]
)
- return scrollHandler
+ return { scrollHandler, enable }
}
type ForwardRefType =
@@ -514,7 +533,7 @@ export function useSharedAnimatedRef(
export function useAfterMountEffect(effect: React.EffectCallback) {
const [didExecute, setDidExecute] = useState(false)
- const result = useEffect(() => {
+ useEffect(() => {
if (didExecute) return
const timeout = setTimeout(() => {
@@ -525,7 +544,6 @@ export function useAfterMountEffect(effect: React.EffectCallback) {
clearTimeout(timeout)
}
}, [didExecute, effect])
- return result
}
export function useConvertAnimatedToValue(
@@ -563,7 +581,7 @@ export function useHeaderMeasurements(): HeaderMeasurements {
const { headerTranslateY, headerHeight } = useTabsContext()
return {
top: headerTranslateY,
- height: headerHeight,
+ height: headerHeight.value || 0,
}
}
diff --git a/src/types.ts b/src/types.ts
index 42645d69..e4f596e7 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -139,8 +139,8 @@ export type CollapsibleProps = {
}
export type ContextType = {
- headerHeight: number
- tabBarHeight: number
+ headerHeight: Animated.SharedValue
+ tabBarHeight: Animated.SharedValue
revealHeaderOnScroll: boolean
snapThreshold: number | null | undefined
/**
@@ -173,7 +173,7 @@ export type ContextType = {
* Array of the scroll y position of each tab.
*/
scrollY: Animated.SharedValue
- containerHeight?: number
+ containerHeight: Animated.SharedValue
/**
* Object containing the ref of each scrollable component.
*/
@@ -219,7 +219,7 @@ export type ContextType = {
*/
contentHeights: Animated.SharedValue
- contentInset: number
+ contentInset: Animated.SharedValue
headerTranslateY: Animated.SharedValue
}