Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ transition={{ type: 'spring', damping: 10 }} → transitionType="spring", tran
- `docs/docs/api-reference.mdx` (API reference table)
- `skills/react-native-ease-refactor/SKILL.md` (supported properties list, transition category keys, decision tree)

## Fixing a Bug

When fixing a reported bug, add a reproducer to the example app so we can manually verify the fix and catch regressions later.

1. Create a new layout under `example/app/issues/<issue-number>/` — use whatever structure the bug needs (tabs, modals, nested stacks). Self-contained so it can be opened directly from the home screen.
2. Register it in `example/src/demos/index.ts` under section `'Issues'` with a `route` field pointing at the new layout (e.g. `route: '/issues/42'`). No `component` is needed — the route owns its own rendering.
3. Include enough context in the screen itself: title, GitHub issue link, repro steps, and visual cues so it's obvious whether the bug is present (e.g. a `mountId` to distinguish "remount" from "still mounted but broken").
4. The reproducer should fail visibly on the broken code path and pass once the fix is in. Don't skip this even when the bug feels obvious — the regression test pays for itself the first time someone touches related code.

## Development Commands

```sh
Expand Down
2 changes: 1 addition & 1 deletion example/app/[demo].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function DemoScreen() {
const { demo } = useLocalSearchParams<{ demo: string }>();
const entry = demos[demo!];

if (!entry) {
if (!entry?.component) {
return null;
}

Expand Down
57 changes: 51 additions & 6 deletions example/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
import { useRouter } from 'expo-router';
import { useMemo, useState } from 'react';
import { SectionList, Text, Pressable, StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { getDemoSections } from '../src/demos';

const sections = getDemoSections();
import { getDemoSections, TABS, type TabKey } from '../src/demos';

export default function HomeScreen() {
const router = useRouter();
const insets = useSafeAreaInsets();
const [tab, setTab] = useState<TabKey>('api');

const sections = useMemo(() => getDemoSections(tab), [tab]);

return (
<SectionList
sections={sections}
keyExtractor={(item) => item.key}
contentContainerStyle={[styles.content, { paddingTop: insets.top }]}
contentContainerStyle={styles.content}
stickySectionHeadersEnabled={false}
ListHeaderComponent={
<View style={styles.header}>
<View style={[styles.header, { paddingTop: insets.top }]}>
<Text style={styles.title}>react-native-ease</Text>
<Text style={styles.subtitle}>
Native animations, zero JS overhead
</Text>
<View style={styles.tabBar}>
{TABS.map(({ key, label }) => {
const active = tab === key;
return (
<Pressable
key={key}
onPress={() => setTab(key)}
style={[styles.tab, active && styles.tabActive]}
>
<Text
style={[styles.tabLabel, active && styles.tabLabelActive]}
>
{label}
</Text>
</Pressable>
);
})}
</View>
</View>
}
renderSectionHeader={({ section }) => (
Expand All @@ -30,7 +50,7 @@ export default function HomeScreen() {
renderItem={({ item }) => (
<Pressable
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
onPress={() => router.push(`/${item.key}`)}
onPress={() => router.push(item.route)}
>
<Text style={styles.rowTitle}>{item.title}</Text>
<Text style={styles.chevron}>›</Text>
Expand Down Expand Up @@ -58,6 +78,31 @@ const styles = StyleSheet.create({
fontSize: 15,
color: '#8888aa',
},
tabBar: {
flexDirection: 'row',
backgroundColor: '#16213e',
borderRadius: 12,
padding: 4,
marginTop: 20,
gap: 4,
},
tab: {
flex: 1,
paddingVertical: 10,
borderRadius: 8,
alignItems: 'center',
},
tabActive: {
backgroundColor: '#2a3a5e',
},
tabLabel: {
fontSize: 14,
fontWeight: '600',
color: '#8888aa',
},
tabLabelActive: {
color: '#fff',
},
sectionHeader: {
fontSize: 13,
fontWeight: '600',
Expand Down
23 changes: 23 additions & 0 deletions example/app/issues/42/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Tabs, Stack } from 'expo-router';

export default function Issue42Layout() {
return (
<>
<Stack.Screen options={{ title: 'Issue #42 — Tabs loop' }} />
<Tabs
screenOptions={{
tabBarActiveTintColor: '#fff',
tabBarInactiveTintColor: '#8888aa',
tabBarStyle: {
backgroundColor: '#1a1a2e',
borderTopColor: '#16213e',
},
headerShown: false,
}}
>
<Tabs.Screen name="index" options={{ title: 'Loop' }} />
<Tabs.Screen name="other" options={{ title: 'Other' }} />
</Tabs>
</>
);
}
125 changes: 125 additions & 0 deletions example/app/issues/42/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { EaseView } from 'react-native-ease';

// Issue #42: Loop animations stop after switching tabs in React Navigation.
// https://github.com/AppAndFlow/react-native-ease/issues/42
//
// Steps to reproduce:
// 1. Observe the spinner and pulse on this tab — both should loop continuously.
// 2. Switch to the "Other" tab.
// 3. Switch back. Without the fix, both animations are frozen.
// 4. Mount-id below should NOT change — the component isn't being remounted;
// only the iOS view detaches from the window, which removes its CAAnimations.

export default function LoopTab() {
const insets = useSafeAreaInsets();
// Stable id per mount — verifies the component is NOT being remounted on tab
// switch. If this number changes when you come back, the bug is unrelated.
const [mountId] = useState(() => Math.floor(Math.random() * 10000));

return (
<View style={[styles.root, { paddingTop: insets.top + 16 }]}>
<Text style={styles.heading}>Issue #42 reproducer</Text>
<Text style={styles.body}>
Switch to the Other tab and back. Both animations should keep looping.
</Text>
<Text style={styles.mountId}>mount #{mountId}</Text>

<View style={styles.row}>
<View style={styles.demo}>
<Text style={styles.demoLabel}>Spin (repeat)</Text>
<EaseView
initialAnimate={{ rotate: 0 }}
animate={{ rotate: 360 }}
transition={{
type: 'timing',
duration: 1000,
easing: 'linear',
loop: 'repeat',
}}
style={styles.box}
>
<View style={styles.indicator} />
</EaseView>
</View>

<View style={styles.demo}>
<Text style={styles.demoLabel}>Pulse (reverse)</Text>
<EaseView
initialAnimate={{ scale: 0.8 }}
animate={{ scale: 1.2 }}
transition={{
type: 'timing',
duration: 600,
easing: 'easeInOut',
loop: 'reverse',
}}
style={styles.circle}
/>
</View>
</View>
</View>
);
}

const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: '#1a1a2e',
paddingHorizontal: 20,
},
heading: {
fontSize: 22,
fontWeight: '700',
color: '#fff',
marginBottom: 8,
},
body: {
fontSize: 14,
color: '#aaaacc',
marginBottom: 8,
lineHeight: 20,
},
mountId: {
fontSize: 12,
fontFamily: 'monospace',
color: '#6666aa',
marginBottom: 32,
},
row: {
flexDirection: 'row',
gap: 24,
justifyContent: 'center',
},
demo: {
alignItems: 'center',
gap: 12,
},
demoLabel: {
fontSize: 13,
color: '#8888aa',
},
box: {
width: 80,
height: 80,
backgroundColor: '#4a90d9',
borderRadius: 12,
alignItems: 'center',
justifyContent: 'flex-start',
paddingTop: 8,
},
indicator: {
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: '#fff',
},
circle: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: '#d94a90',
},
});
57 changes: 57 additions & 0 deletions example/app/issues/42/other.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export default function OtherTab() {
const insets = useSafeAreaInsets();
return (
<ScrollView
style={styles.root}
contentContainerStyle={[
styles.content,
{ paddingTop: insets.top + 16, paddingBottom: insets.bottom + 24 },
]}
>
<Text style={styles.heading}>Other tab</Text>
<Text style={styles.body}>
Switch back to the Loop tab. Animations should still be running.
</Text>
{Array.from({ length: 30 }).map((_, i) => (
<View key={i} style={styles.item}>
<Text style={styles.itemText}>Item {i + 1}</Text>
</View>
))}
</ScrollView>
);
}

const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: '#1a1a2e',
},
content: {
paddingHorizontal: 20,
gap: 8,
},
heading: {
fontSize: 22,
fontWeight: '700',
color: '#fff',
marginBottom: 8,
},
body: {
fontSize: 14,
color: '#aaaacc',
marginBottom: 16,
lineHeight: 20,
},
item: {
padding: 16,
backgroundColor: '#16213e',
borderRadius: 12,
},
itemText: {
color: '#e0e0ff',
fontSize: 15,
},
});
Loading
Loading