Skip to content

Commit 0479d4d

Browse files
committed
feat: use context to provide more convinient way to interact with carousel
1 parent 0378473 commit 0479d4d

File tree

9 files changed

+220
-64
lines changed

9 files changed

+220
-64
lines changed

README.md

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -64,55 +64,66 @@ Note: Currently, I am using `react-native-reanimated` for animation. So you shou
6464
```
6565
## Usage
6666
```javascript
67-
import Carousel from '@r0b0t3d/react-native-carousel';
67+
import Carousel, {
68+
withCarouselContext,
69+
useCarouselContext,
70+
} from '@r0b0t3d/react-native-carousel';
6871

6972
function MyCarousel() {
70-
const currentPage = useSharedValue(0);
73+
const {
74+
goNext,
75+
goPrev,
76+
snapToItem
77+
} = useCarouselContext(); // <- use this instead of passing ref to Carousel
78+
7179
return (
7280
<View>
73-
<Carousel
74-
style={{ height: 200 }}
75-
data={data}
76-
loop={false}
77-
autoPlay={true}
78-
duration={3000}
79-
itemWidth={width - 100}
80-
inactiveOpacity={0.5}
81-
inactiveScale={0.9}
82-
firstItemAlignment="start"
83-
spaceBetween={20}
84-
animatedPage={currentPage}
85-
renderItem={({item}) => {
86-
return (
87-
<Image
88-
style={{
89-
flex: 1,
90-
backgroundColor: 'red',
91-
}}
92-
source={{ uri: item.url }}
93-
/>
94-
);
95-
}}
96-
/>
97-
<View>
98-
<PaginationIndicator
99-
totalPage={data.length}
100-
currentPage={currentPage}
101-
containerStyle={{ marginTop: 20 }}
102-
activeIndicatorStyle={{
103-
width: 20,
104-
height: 10,
105-
borderRadius: 5,
106-
}}
107-
indicatorConfigs={{
108-
spaceBetween: 10
81+
<Carousel
82+
style={{ height: 200 }}
83+
data={data}
84+
loop={false}
85+
autoPlay={true}
86+
duration={3000}
87+
itemWidth={width - 100}
88+
inactiveOpacity={0.5}
89+
inactiveScale={0.9}
90+
firstItemAlignment="start"
91+
spaceBetween={20}
92+
animatedPage={currentPage}
93+
renderItem={({item}) => {
94+
return (
95+
<Image
96+
style={{
97+
flex: 1,
98+
backgroundColor: 'red',
99+
}}
100+
source={{ uri: item.url }}
101+
/>
102+
);
109103
}}
110104
/>
105+
<View>
106+
<PaginationIndicator
107+
containerStyle={{ marginTop: 20 }}
108+
activeIndicatorStyle={{
109+
height: 10,
110+
borderRadius: 5,
111+
}}
112+
indicatorConfigs={{
113+
spaceBetween: 10,
114+
indicatorWidth: 10,
115+
indicatorSelectedWidth: 20,
116+
}}
117+
/>
118+
</View>
111119
</View>
112-
</View>
120+
);
113121
}
122+
123+
export default withCarouselContext(MyCarousel) // <-- To use carousel context, you need wrap your component with withCarouselContext
114124
```
115125

126+
# Carousel
116127
## Properties
117128

118129
| Props | Description | Default |
@@ -142,6 +153,17 @@ function MyCarousel() {
142153
| goNext | Go to next index |
143154
| goPrev | Go to previous index |
144155
| snapToItem | `(index: number, animated?: boolean) => void`<br>Snap to specific index <br>- `index`: destination index<br>- `animated`: should animate or not, default is `true` |
156+
### withCarouselContext
157+
This HOC provides easy way to wrap your component with `CarouselContext.Provider`.
158+
So if you'd like to use `useCarouselContext`, you need to wrap your component with this.
159+
160+
# PaginationIndicator
161+
Easy way to define the indicator for your carousel.
162+
163+
Please note that, this component only works with `withCarouselContext`.
164+
So please make sure that it is rendered under the component that you wrap with `withCarouselContext`
165+
166+
Check example above for more info
145167
## Contributing
146168

147169
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.

example/src/App.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
import Carousel, {
2222
CarouselHandles,
2323
PaginationIndicator,
24+
withCarouselContext,
25+
useCarouselContext,
2426
} from '@r0b0t3d/react-native-carousel';
2527
import { useSharedValue } from 'react-native-reanimated';
2628

@@ -78,27 +80,25 @@ const data: CarouselData[] = [
7880

7981
const { width } = Dimensions.get('window');
8082

81-
export default function App() {
82-
const currentPage = useSharedValue(0);
83-
const carousel = useRef<CarouselHandles>(null);
83+
function App() {
84+
const { goNext, goPrev, snapToItem } = useCarouselContext();
8485

8586
const handleRandom = useCallback(() => {
8687
const randomIdx = Math.floor(Math.random() * data.length);
87-
carousel.current?.snapToItem(randomIdx, true);
88+
snapToItem(randomIdx, true);
8889
}, []);
8990

9091
const handleNext = useCallback(() => {
91-
carousel.current?.goNext();
92+
goNext();
9293
}, []);
9394

9495
const handlePrev = useCallback(() => {
95-
carousel.current?.goPrev();
96+
goPrev();
9697
}, []);
9798

9899
return (
99100
<View style={styles.container}>
100101
<Carousel
101-
ref={carousel}
102102
style={{ height: 200 }}
103103
initialPage={2}
104104
data={data}
@@ -111,7 +111,6 @@ export default function App() {
111111
firstItemAlignment="center"
112112
spaceBetween={10}
113113
spaceHeadTail={20}
114-
animatedPage={currentPage}
115114
additionalPagesPerSide={3}
116115
scrollViewProps={{
117116
scrollEnabled: true,
@@ -131,7 +130,6 @@ export default function App() {
131130
<View>
132131
<PaginationIndicator
133132
totalPage={data.length}
134-
currentPage={currentPage}
135133
containerStyle={{ marginTop: 20 }}
136134
activeIndicatorStyle={{
137135
height: 20,
@@ -160,6 +158,8 @@ export default function App() {
160158
);
161159
}
162160

161+
export default withCarouselContext(App);
162+
163163
const styles = StyleSheet.create({
164164
container: {
165165
flex: 1,

src/components/Carousel.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/ban-ts-comment */
12
import React, {
23
useState,
34
useRef,
@@ -18,6 +19,8 @@ import Animated, {
1819
import type { CarouselProps, CarouselHandles } from '../types';
1920
import PageItem from './PageItem';
2021
import { findNearestPage, generateOffsets } from '../utils';
22+
import { useCarouselContext } from './useCarouselContext';
23+
import { useInternalCarouselContext } from './useInternalCarouselContext';
2124

2225
const { width: wWidth } = Dimensions.get('screen');
2326

@@ -40,7 +43,6 @@ function Carousel<TData>(
4043
spaceHeadTail = 0,
4144
renderItem,
4245
onPageChange,
43-
animatedPage = useSharedValue(0),
4446
scrollViewProps = {},
4547
keyExtractor,
4648
}: CarouselProps<TData>,
@@ -52,7 +54,8 @@ function Carousel<TData>(
5254
const [isDragging, setDragging] = useState(false);
5355
const expectedPosition = useRef(-1);
5456
const pageMapper = useRef<Record<number, number>>({});
55-
57+
const { currentPage: animatedPage, totalPage } = useCarouselContext();
58+
5659
const horizontalPadding = useMemo(() => {
5760
const padding = (sliderWidth - itemWidth) / 2;
5861
return firstItemAlignment === 'center' || loop ? padding : spaceHeadTail;
@@ -76,6 +79,10 @@ function Carousel<TData>(
7679
}, [sliderWidth, itemWidth, data, horizontalPadding]);
7780

7881
const pageItems = useMemo(() => {
82+
if (!data) {
83+
return [];
84+
}
85+
totalPage.value = data.length;
7986
if (loop) {
8087
const headItems = data.slice(
8188
data.length - additionalPagesPerSide,
@@ -131,7 +138,7 @@ function Carousel<TData>(
131138
[handleScrollTo, animatedScroll, freeze]
132139
);
133140

134-
const goNext = useCallback(() => {
141+
const goNext = useCallback(() => {
135142
const next = currentPage.value + 1;
136143
handleScrollTo(next);
137144
}, [handleScrollTo]);
@@ -160,13 +167,25 @@ function Carousel<TData>(
160167
[handleScrollTo]
161168
);
162169

170+
const { setCarouselHandlers } = useInternalCarouselContext();
171+
172+
useEffect(() => {
173+
if (setCarouselHandlers) {
174+
setCarouselHandlers({
175+
goNext,
176+
goPrev,
177+
snapToItem,
178+
});
179+
}
180+
}, [goNext, goPrev, snapToItem, setCarouselHandlers]);
181+
163182
const handlePageChange = useCallback(
164183
(page: number) => {
165184
const actualPage = getActualPage(page);
166185
animatedPage.value = actualPage;
167186
if (onPageChange) {
168187
onPageChange(actualPage);
169-
}
188+
}
170189
if (!loop) return;
171190
if (page === pageItems.length - 1) {
172191
jumpTo(additionalPagesPerSide * 2 - 1);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, {
2+
FC,
3+
ReactNode,
4+
useCallback,
5+
useMemo,
6+
useRef,
7+
useState,
8+
} from 'react';
9+
import Animated, { useSharedValue } from 'react-native-reanimated';
10+
import type { CarouselHandles } from 'src/types';
11+
import { CarouselContext } from './useCarouselContext';
12+
import { InternalCarouselContext } from './useInternalCarouselContext';
13+
14+
type Props = {
15+
children: ReactNode;
16+
};
17+
18+
function CarouselContainer({ children }: Props) {
19+
const carouselHandlers = useRef<CarouselHandles>();
20+
const currentPage = useSharedValue(0);
21+
const totalPage = useSharedValue(0);
22+
23+
const setCarouselHandlers = useCallback((handlers) => {
24+
carouselHandlers.current = handlers;
25+
}, []);
26+
27+
const context = useMemo(() => {
28+
return {
29+
goNext: () => carouselHandlers.current?.goNext(),
30+
goPrev: () => carouselHandlers.current?.goPrev(),
31+
snapToItem: (index: number, animated: boolean) =>
32+
carouselHandlers.current?.snapToItem(index, animated),
33+
currentPage,
34+
totalPage,
35+
};
36+
}, []);
37+
38+
const internalContext = useMemo(
39+
() => ({
40+
setCarouselHandlers,
41+
}),
42+
[]
43+
);
44+
45+
return (
46+
<CarouselContext.Provider value={context}>
47+
<InternalCarouselContext.Provider value={internalContext}>
48+
{children}
49+
</InternalCarouselContext.Provider>
50+
</CarouselContext.Provider>
51+
);
52+
}
53+
54+
export default function withCarouselContext<T>(Component: FC<T>) {
55+
return (props: T) => {
56+
return (
57+
<CarouselContainer>
58+
<Component {...props} />
59+
</CarouselContainer>
60+
);
61+
};
62+
}

0 commit comments

Comments
 (0)