-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
refactor: simplify Tabs component #22284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
b9e821f to
9bf41a3
Compare
| /** | ||
| * Callback when tab layout changes | ||
| */ | ||
| onLayout?: (event: LayoutChangeEvent) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not used
| const handleOnLayout = useCallback( | ||
| (layoutEvent: Parameters<NonNullable<typeof onLayout>>[0]) => { | ||
| if (onLayout) { | ||
| onLayout(layoutEvent); | ||
| } | ||
| }, | ||
| [onLayout], | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No longer needed
| useEffect(() => { | ||
| // Skip animation on initial mount - just set the value | ||
| if (isInitialMount.current) { | ||
| isInitialMount.current = false; | ||
| scaleAnim.setValue(isActive && !isDisabled ? 1 : 0); | ||
| return; | ||
| } | ||
|
|
||
| // Animate on subsequent changes | ||
| Animated.timing(scaleAnim, { | ||
| toValue: isActive && !isDisabled ? 1 : 0, | ||
| duration: AnimationDuration.Fast, | ||
| useNativeDriver: true, | ||
| }).start(); | ||
| }, [isActive, isDisabled, scaleAnim]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handles animation of underline
| <View | ||
| ref={viewRef} | ||
| onLayout={handleOnLayout} | ||
| style={tw.style('flex-shrink-0')} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reducing element bloat by removing wrapping View as it's no longer needed
| {/* Animated underline */} | ||
| <Animated.View | ||
| style={[ | ||
| tw.style('absolute bottom-0 left-0 right-0 h-0.5 bg-icon-default'), | ||
| { | ||
| transform: [{ scaleX: scaleAnim }], | ||
| }, | ||
| ]} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved underline into the Tab component. A bit of flexibility on the animation style here
| <RCTScrollView | ||
| contentContainerStyle={ | ||
| { | ||
| "flexDirection": "row", | ||
| "gap": 24, | ||
| "paddingLeft": 16, | ||
| "paddingRight": 16, | ||
| } | ||
| } | ||
| horizontal={true} | ||
| onContentSizeChange={[Function]} | ||
| scrollEnabled={false} | ||
| scrollsToTop={false} | ||
| showsHorizontalScrollIndicator={false} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default renders the ScrollView so we get quite large snapshot updates
| @@ -1,20 +1,10 @@ | |||
| // Third party dependencies. | |||
| import React, { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we can remove all the complex logic that handled the underline animation
| <ScrollView | ||
| horizontal | ||
| showsHorizontalScrollIndicator={false} | ||
| scrollEnabled={scrollEnabled} | ||
| style={tw.style('flex-grow-0')} | ||
| contentContainerStyle={tw.style('flex-row px-4 gap-6')} | ||
| scrollsToTop={false} | ||
| onContentSizeChange={handleContentSizeChange} | ||
| > | ||
| {tabs.map((tab, index) => ( | ||
| <Tab | ||
| key={tab.key} | ||
| label={tab.label} | ||
| isActive={index === activeIndex} | ||
| isDisabled={tab.isDisabled} | ||
| onPress={() => handleTabPress(index)} | ||
| testID={`${testID}-tab-${index}`} | ||
| /> | ||
| ))} | ||
| </ScrollView> | ||
| </Box> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing a lot of duplicated code and the scrollEnabled in favor of scrollEnabled via the ScrollView. We check if this is enabled by checking the content of ScrollView against the width of the wrapping Box component which is calculated via the handleContainerLayout
Fixes a bug where the Tab animation would run on initial mount even when already at the target state. This caused unnecessary animation work and potential visual glitches. Changes: - Added isInitialMount ref to track first render - Skip animation on mount, immediately set value instead - Subsequent changes animate normally This ensures underlines appear instantly on first render and only animate when the active state actually changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
6eef084 to
17c3018
Compare
|



Description
This PR significantly refactors the Tabs component to reduce complexity improve maintainability. The refactoring removes ~200 lines of complex animation code while introducing a simpler, more performant animation system.
What is the reason for the change?
What is the improvement/solution?
contentContainerStyleso scroll detection works automatically without hardcoded valuesChangelog
CHANGELOG entry: null
Related issues
Fixes: N/A (internal refactoring)
Manual testing steps
Screenshots/Recordings
Before
Complex animation system with manual underline positioning across the entire TabsBar
tabsbefore.mov
After
Simplified animation with each tab handling its own underline, growing from center when active
after480.mov
Performance Impact
Yes, this improves performance:
useNativeDriver: true, which runs animations on the native thread instead of the JS thread, providing 60fps animations even when JS is busyrequestAnimationFrameusage that was previously polling for layout changesCode reduction:
Pre-merge author checklist
Pre-merge reviewer checklist
Note
Reworks Tabs to use per-Tab underline animation and simplified scroll detection, removing complex layout/animation logic from TabsBar.
Tabs/Tab:ViewwithPressableand adds animated underline (Animated.View) usingscaleXwith native driver.isDisabled.onLayouthandling and related props; importsAnimationDuration.Tabs/TabsBar:style, drops...boxProps).ScrollViewwith padding/gap incontentContainerStyleand enables scrolling based on container width vs content size.Tab.types.ts: DropsonLayoutprop andLayoutChangeEventimport.Written by Cursor Bugbot for commit 9bf41a3. This will update automatically on new commits. Configure here.