Migration Guide to v5
Migrate with your AI agent
Read https://rn-carousel.dev/migration-v5.md and upgrade react-native-reanimated-carousel to v5 in this project, then summarize what changed for my review.The agent only edits files in your working tree — nothing is committed, and it stops for your review.
This document is an execution manual for an AI coding agent upgrading a project from react-native-reanimated-carousel v4 to v5. Follow the protocol in order, preserve the application's existing product behavior unless a human approves a change, and stop at the review boundary.
Step 1: run the codemod for mechanical changes
First update the project dependency to v5 with the package manager already used by the project. Keep these peer requirements satisfied:
{
"react": ">=18.0.0",
"react-native": ">=0.80.0",
"react-native-gesture-handler": ">=2.9.0 <3.0.0",
"react-native-reanimated": ">=4.1.0",
"react-native-worklets": ">=0.5.0"
}Expo projects should let Expo choose compatible peer versions:
npx expo install react-native-reanimated-carousel react-native-reanimated react-native-worklets react-native-gesture-handlerThen run the v5 codemod from the project root:
npx rnrc-v5-codemod .Use --dry first when the working tree already contains unrelated edits:
npx rnrc-v5-codemod --dry .The codemod performs deterministic changes only:
- converts the default
Carouselimport to a named import; - renames direct Carousel props and public type imports;
- converts
vertical,autoPlayReverse, item sizing, and snap booleans; - adds
loop={true}wherever v4 code relied on the old default; - reports APIs that require a product decision instead of guessing.
It is idempotent. Review every warning before continuing.
Mechanical rename reference
- import Carousel, { ICarouselInstance } from "react-native-reanimated-carousel";
+ import { Carousel, type CarouselRef } from "react-native-reanimated-carousel";
- const ref = React.useRef<ICarouselInstance>(null);
+ const ref = React.useRef<CarouselRef>(null);| v4 | v5 |
|---|---|
default Carousel export | named Carousel |
TCarouselProps | CarouselProps |
ICarouselInstance | CarouselRef |
IComputedDirectionTypes | removed |
TAnimationStyle | CarouselItemAnimation |
ILayoutConfig | CarouselLayout |
width, height | style.width, style.height |
vertical | orientation="vertical" |
itemWidth, itemHeight | itemSize |
enabled | scrollEnabled |
pagingEnabled + snapEnabled / enableSnap | snapMode="page" | "nearest" | "none" |
windowSize | renderWindowSize |
autoPlay | autoplay |
autoPlayInterval | autoplayInterval |
autoPlayReverse | autoplayDirection="backward" |
customAnimation | itemAnimation |
render-item animationValue | relativeProgress |
scrollAnimationDuration + withAnimation | animation |
mode + modeConfig | layout |
defaultScrollOffsetValue | scrollOffsetValue |
SharedValue-form onProgressChange={value} | progress={value} |
onScrollEnd | removed; use settled onSnapToItem |
scrollTo({ count }) | next({ count }) or prev({ count }) |
command onFinished | removed |
fixedDirection | removed; use next or prev |
customConfig, CustomConfig | removed |
autoFillData | removed; short-loop handling is internal |
minScrollDistancePerSwipe, maxScrollDistancePerSwipe | gesture thresholds through onConfigurePanGesture |
The package root exports only Carousel, Pagination, and the documented v5 type whitelist. Default imports, compatibility aliases, and deep imports are unsupported.
Manual layout migration
The codemod reports mode and modeConfig; convert them together:
- mode="parallax"
- modeConfig={{
- parallaxScrollingOffset: 100,
- parallaxScrollingScale: 0.8,
- parallaxAdjacentItemScale: 0.64,
- }}
+ layout={{
+ type: "parallax",
+ offset: 100,
+ scale: 0.8,
+ adjacentScale: 0.64,
+ }}| v4 stack field | v5 stack field |
|---|---|
showLength | visibleCount |
moveSize | exitDistance |
stackInterval | spacing |
scaleInterval | scaleStep |
opacityInterval | opacityStep |
rotateZDeg | rotation |
snapDirection | exitDirection |
layout and itemAnimation are mutually exclusive. Preserve the old stack runtime defaults when the app depends on them: physical left exit, screen-width distance, 18 spacing, 0.04 scale step, 0.1 opacity step, and 30 degrees rotation.
Manual Pagination migration
- <Pagination.Basic
- data={data}
- progress={progress}
- size={8}
- />
+ <Pagination
+ count={data.length}
+ progress={progress}
+ dotStyle={{ width: 8, height: 8 }}
+ />| v4 | v5 |
|---|---|
Pagination.Basic, Pagination.Custom | Pagination |
data | count |
horizontal | orientation |
size | dotStyle.width / dotStyle.height |
carouselName | removed |
paginationItemAccessibility | getItemAccessibilityLabel |
renderItem, generic data | removed |
customReanimatedStyle | removed; build a custom indicator from progress |
Add onPress when the dots should be interactive. Without it, v5 treats the dots as decorative and hides them from the accessibility tree.
Stable item identity
Add keyExtractor when items have stable identities:
<Carousel
data={products}
keyExtractor={(product) => product.id}
renderItem={renderProduct}
/>When data changes, v5 keeps the selected key when possible. Without keyExtractor, or when the key disappears, it clamps the previous numeric index after active movement settles.
Step 2: review all 12 behavioral changes
Do not treat a successful typecheck as proof that the migration is complete. Check every item below against the product's intended behavior and record the answer to every Human confirmation required line.
1. Loop now defaults to false
Before, omitting loop enabled wrapping:
<Carousel data={data} renderItem={renderItem} />After, preserve the v4 behavior explicitly or choose the new finite behavior:
<Carousel data={data} renderItem={renderItem} loop={true} />Human confirmation required: Should this carousel wrap forever? The codemod inserts loop={true} to preserve v4 behavior, but a human must confirm the product intent.
2. The default autoplay dwell changes from 1000ms to 3000ms
Before:
<Carousel autoPlay data={data} renderItem={renderItem} />After, preserve the old cadence explicitly when required:
<Carousel autoplay autoplayInterval={1000} data={data} renderItem={renderItem} />The interval still begins after the previous transition settles.
Human confirmation required: Should the product keep a 1000ms dwell or adopt the new 3000ms default?
3. Every ref command now defaults to animated
Before, v4 scrollTo was immediate unless animated was set:
ref.current?.scrollTo({ index: 2 });After, preserve an immediate jump explicitly:
ref.current?.scrollTo({ index: 2, animated: false });next, prev, and scrollTo all default to animated: true.
Human confirmation required: For each scrollTo call that omitted animated, should it jump immediately or animate?
4. Looping scrollTo uses the shortest route
Before, route choice could reuse the previous travel direction:
ref.current?.scrollTo({ index: 0 });After, the same call chooses the shortest loop route and chooses logical forward on an exact tie:
ref.current?.scrollTo({ index: 0 });Use next or prev when direction is the product requirement.
Human confirmation required: Does any animation, onboarding sequence, or analytics flow depend on a specific travel direction?
5. getCurrentIndex() returns the last settled index
Before, an in-flight read could return a rounded visual index:
const inFlightIndex = ref.current?.getCurrentIndex();After, use progress for live visual position and keep getCurrentIndex() for settled state:
<Carousel
progress={progress}
onSnapToItem={(index) => setSelectedIndex(index)}
/>Human confirmation required: Does any caller need the in-flight visual index rather than the last settled selection?
6. Loop progress is continuous and unbounded
Before, loop progress was normalized to one data cycle:
<Pagination.Basic data={data} progress={progress} />After, the same logical position can be -1, count + 1, or many cycles away:
<Carousel progress={progress} data={data} renderItem={renderItem} loop />
<Pagination count={data.length} progress={progress} />The v5 Pagination handles nearest-cycle interpolation internally. Normalize only custom consumers that truly require [0, count).
Human confirmation required: Does any custom progress consumer assume a bounded range?
7. onProgressChange receives one logical progress value
Before:
<Carousel
onProgressChange={(offsetPx, absoluteProgress) => {
update(offsetPx, absoluteProgress);
}}
/>After:
const offset = useSharedValue(0);
<Carousel
scrollOffsetValue={offset}
onProgressChange={(progress) => {
updateProgress(progress);
}}
/>The callback runs on the JS thread every frame; use progress or scrollOffsetValue SharedValues for UI-thread work.
Human confirmation required: Which consumers need logical progress, and which still need signed pixel translation?
8. Pagination visuals and accessibility semantics change
Before:
<Pagination.Basic data={data} progress={progress} />After, non-interactive dots are decorative:
<Pagination count={data.length} progress={progress} />To keep them interactive, wire explicit navigation:
<Pagination
count={data.length}
progress={progress}
onPress={(index) => ref.current?.scrollTo({ index })}
/>Human confirmation required: Are the dots navigation controls or decorative status, and must a custom v4 visual be recreated?
9. Only the current slide is exposed to accessibility services
Before, off-screen slide content could remain in the accessibility tree:
<Carousel data={data} renderItem={renderItem} />After, v5 hides non-current slides. Add explicit external navigation when users need to move between slides without swiping:
<Button title="Previous" onPress={() => ref.current?.prev()} />
<Button title="Next" onPress={() => ref.current?.next()} />Human confirmation required: Is the current slide understandable on its own, and does the screen need accessible previous/next controls?
10. Animation duration no longer includes a hidden extra 100ms
Before, this declaration settled in roughly 500ms:
<Carousel scrollAnimationDuration={400} />After, preserve the old perceived duration explicitly or adopt the exact duration:
<Carousel animation={{ type: "timing", duration: 500 }} />Human confirmation required: Should the interaction preserve the old perceived timing or use the exact declared duration?
11. Accepted non-animated commands emit a complete lifecycle
Before, an immediate command did not emit a settled onScrollEnd event:
<Carousel onScrollEnd={handleSettled} />
ref.current?.scrollTo({ index: 2, animated: false });After, it fires onScrollStart and then immediately fires onSnapToItem:
<Carousel onScrollStart={handleStart} onSnapToItem={handleSettled} />
ref.current?.scrollTo({ index: 2, animated: false });Human confirmation required: Will analytics, state synchronization, or side effects double-count immediate navigation?
12. scrollEnabled disables gestures only
Before:
<Carousel enabled={false} autoPlay />After:
<Carousel scrollEnabled={false} autoplay />Autoplay and ref commands still work. Disable autoplay separately when the whole carousel must pause.
Human confirmation required: Should disabling user gestures also pause autoplay or external controls in this screen?
Also review onScrollStart: v5 fires it for gestures, ref commands, and autoplay. Do not toggle autoplay from this callback because that can create a feedback loop. Use an onBegin worklet through onConfigurePanGesture when user contact specifically matters.
Step 3: run the allowed validation
Inspect the project's existing scripts and run only:
- its typecheck;
- its production build;
- its existing unit tests, if they can be run directly in the current environment.
Fix migration-caused failures and rerun the relevant command. If a command is unavailable or blocked by an existing environment requirement, report that limitation instead of expanding the scope.
Do NOT run end-to-end tests. Do NOT launch simulators, emulators, or devices. Do NOT start the app.
Finish: summarize and stop
Return:
- a concise summary of changed files and API migrations;
- the result of the typecheck, build, and existing unit tests;
- a checklist of every item that still needs human confirmation;
- any codemod warnings or validation limitations.
Then stop and wait for the user to review the working tree.
Do NOT commit or push — leave all changes uncommitted for the user to review.