BreathingExerciseProvider
The BreathingExerciseProvider is a React Context provider that manages the state and timing logic for breathing exercises. It provides real-time progress tracking, phase transitions, and animation synchronization using React Native Reanimated for optimal performance.
Overview
BreathingExerciseProvider serves as the central state management solution for breathing exercises. It handles complex timing calculations, phase transitions, and provides shared values for smooth animations. The provider automatically manages the breathing cycle progression and exposes the current state through the useBreathingExercise hook.
Features
- 🕒 Precise Timing Control: Accurate phase timing with millisecond precision
- 🔄 Automatic Phase Transitions: Seamless transitions between inhale, hold, and exhale
- 📊 Real-time Progress Tracking: Live progress updates using shared values
- 🎨 Animation Synchronization: Coordinated animation progress with breathing phases
- ⚡ Performance Optimized: Runs calculations on UI thread using React Native Reanimated
- 🧮 Derived State Calculations: Efficient state derivation for optimal re-renders
- 🎯 Completion Handling: Automatic exercise completion detection and callbacks
- 🔧 Flexible Configuration: Support for custom breathing patterns and cycle counts
API Reference
Props
interface BreathingExerciseProviderProps extends React.PropsWithChildren {
methods: BreathingPattern;
repeat: number;
onFinish?: () => void;
}
type BreathingPattern = [
{ type: "inhale"; duration: number },
{ type: "hold"; duration: number },
{ type: "exhale"; duration: number },
];
| Prop | Type | Required | Description |
|---|---|---|---|
children | React.ReactNode | ✅ | Child components that will consume the context |
methods | BreathingPattern | ✅ | Array defining inhale, hold, and exhale durations |
repeat | number | ✅ | Number of complete breathing cycles |
onFinish | () => void | ❌ | Callback executed when exercise completes |
Context Value
The provider exposes the following through useBreathingExercise():
interface BreathingExerciseContextType {
// Configuration
methods: BreathingPattern;
repeat: number;
onFinish?: () => void;
// Real-time State
progress: SharedValue<number>;
roundData: SharedValue<{
method: BreathingPattern[number];
animationProgress: number;
remainingTime: number;
}>;
remainingTime: number;
methodType: "inhale" | "hold" | "exhale";
}
Basic Usage
Simple Provider Setup
import {
BreathingExerciseProvider,
useBreathingExercise,
} from "@breathing-exercise";
const BreathingDisplay = () => {
const { methodType, remainingTime, progress } = useBreathingExercise();
return (
<View>
<Text>Phase: {methodType}</Text>
<Text>Time Left: {remainingTime}s</Text>
<Text>Progress: {Math.round(progress.value * 100)}%</Text>
</View>
);
};
export const BasicBreathingSession = () => {
const handleComplete = () => {
console.log("Breathing exercise completed!");
};
return (
<BreathingExerciseProvider
methods={[
{ type: "inhale", duration: 4 },
{ type: "hold", duration: 4 },
{ type: "exhale", duration: 4 },
]}
repeat={5}
onFinish={handleComplete}
>
<BreathingDisplay />
</BreathingExerciseProvider>
);
};
Multiple Consumers
const ProgressIndicator = () => {
const { progress } = useBreathingExercise();
return (
<Animated.View
style={[styles.progressBar, { width: `${progress.value * 100}%` }]}
/>
);
};
const PhaseDisplay = () => {
const { methodType, remainingTime } = useBreathingExercise();
return (
<View style={styles.phaseContainer}>
<Text style={styles.phaseText}>{methodType}</Text>
<Text style={styles.timeText}>{Math.ceil(remainingTime)}s</Text>
</View>
);
};
const AnimationController = () => {
const { roundData } = useBreathingExercise();
return (
<LottieView
source={breathingAnimation}
progress={roundData.value.animationProgress}
style={styles.animation}
/>
);
};
export const MultiConsumerBreathingSession = () => (
<BreathingExerciseProvider
methods={[
{ type: "inhale", duration: 4 },
{ type: "hold", duration: 7 },
{ type: "exhale", duration: 8 },
]}
repeat={8}
onFinish={() => navigation.navigate("Completion")}
>
<ProgressIndicator />
<PhaseDisplay />
<AnimationController />
</BreathingExerciseProvider>
);
Advanced Usage
Custom Progress Tracking
const AdvancedProgressTracker = () => {
const { progress, roundData, methods, repeat } = useBreathingExercise();
const [sessionStats, setSessionStats] = React.useState({
startTime: Date.now(),
phaseChanges: 0,
currentRound: 0,
});
// Track phase changes
const previousPhase = React.useRef<string>();
React.useEffect(() => {
const currentPhase = roundData.value.method.type;
if (previousPhase.current && previousPhase.current !== currentPhase) {
setSessionStats((prev) => ({
...prev,
phaseChanges: prev.phaseChanges + 1,
}));
}
previousPhase.current = currentPhase;
}, [roundData.value.method.type]);
// Calculate current round
const currentRound = Math.floor(progress.value * repeat) + 1;
React.useEffect(() => {
setSessionStats((prev) => ({
...prev,
currentRound: Math.min(currentRound, repeat),
}));
}, [currentRound, repeat]);
return (
<View style={styles.statsContainer}>
<Text>
Round: {sessionStats.currentRound} / {repeat}
</Text>
<Text>Phase Changes: {sessionStats.phaseChanges}</Text>
<Text>
Session Time: {Math.floor((Date.now() - sessionStats.startTime) / 1000)}
s
</Text>
</View>
);
};
Animation Integration
import Animated, {
useAnimatedStyle,
interpolate,
useDerivedValue,
} from "react-native-reanimated";
const SynchronizedBreathingVisual = () => {
const { roundData, progress } = useBreathingExercise();
// Breathing circle animation
const breathingCircleStyle = useAnimatedStyle(() => {
const scale = interpolate(
roundData.value.animationProgress,
[0, 0.33, 0.66, 1],
[1, 1.5, 1.5, 1], // Scale up on inhale, maintain on hold, scale down on exhale
);
return {
transform: [{ scale }],
};
});
// Progress ring animation
const progressRingStyle = useAnimatedStyle(() => {
const rotation = interpolate(progress.value, [0, 1], [0, 360]);
return {
transform: [{ rotate: `${rotation}deg` }],
};
});
// Background color animation
const backgroundStyle = useAnimatedStyle(() => {
const method = roundData.value.method.type;
const colors = {
inhale: "#e3f2fd",
hold: "#f3e5f5",
exhale: "#e8f5e8",
};
return {
backgroundColor: colors[method] || "#f5f5f5",
};
});
return (
<Animated.View style={[styles.container, backgroundStyle]}>
<Animated.View style={[styles.progressRing, progressRingStyle]} />
<Animated.View style={[styles.breathingCircle, breathingCircleStyle]} />
</Animated.View>
);
};
State Persistence
import AsyncStorage from "@react-native-async-storage/async-storage";
const PersistentBreathingSession = ({ sessionId, ...props }) => {
const [savedProgress, setSavedProgress] = React.useState(0);
// Load saved progress
React.useEffect(() => {
const loadProgress = async () => {
try {
const saved = await AsyncStorage.getItem(
`breathing_session_${sessionId}`,
);
if (saved) {
setSavedProgress(parseFloat(saved));
}
} catch (error) {
console.error("Failed to load progress:", error);
}
};
loadProgress();
}, [sessionId]);
const handleFinish = React.useCallback(async () => {
try {
// Clear saved progress
await AsyncStorage.removeItem(`breathing_session_${sessionId}`);
// Save completion data
await AsyncStorage.setItem(
`breathing_completed_${sessionId}`,
JSON.stringify({
completedAt: new Date().toISOString(),
pattern: props.methods,
cycles: props.repeat,
}),
);
props.onFinish?.();
} catch (error) {
console.error("Failed to save completion:", error);
}
}, [sessionId, props]);
return (
<BreathingExerciseProvider {...props} onFinish={handleFinish}>
<ProgressPersistence sessionId={sessionId} />
{props.children}
</BreathingExerciseProvider>
);
};
const ProgressPersistence = ({ sessionId }) => {
const { progress } = useBreathingExercise();
// Save progress periodically
React.useEffect(() => {
const saveProgress = async () => {
try {
await AsyncStorage.setItem(
`breathing_session_${sessionId}`,
progress.value.toString(),
);
} catch (error) {
console.error("Failed to save progress:", error);
}
};
const interval = setInterval(saveProgress, 5000); // Save every 5 seconds
return () => clearInterval(interval);
}, [sessionId, progress]);
return null;
};
State Management
Internal State Structure
interface InternalState {
methodType: "inhale" | "hold" | "exhale";
remainingTime: number;
}
The provider maintains minimal React state while leveraging shared values for performance-critical updates:
- React State: Used for component re-renders (
methodType,remainingTime) - Shared Values: Used for animations and derived calculations (
progress,roundData)
State Flow
- Initialization: Provider sets up shared values and initial state
- Progress Updates: Shared
progressvalue drives all calculations - Phase Detection:
roundDatadetermines current breathing phase - State Synchronization: React state updates when phase changes
- Completion:
onFinishcallback triggered when progress reaches 1
Derived State Calculations
// Simplified version of internal logic
const roundData = useDerivedValue(() => {
const isCompleted = progress.value >= 1;
if (isCompleted) {
return { /* completion state */ };
}
// Calculate current position in breathing cycle
const durationInRound = (progress.value * roundDuration) % roundDuration;
// Find current method (inhale/hold/exhale)
const currentMethodIndex = methodDurations.findIndex(
duration => duration > durationInRound
);
// Calculate animation progress for current method
const animationProgress = /* complex calculation */;
return {
method: methods[currentMethodIndex],
animationProgress,
remainingTime: /* remaining time for current phase */,
};
});
Performance
Optimization Strategies
// ✅ Good: Memoize expensive calculations
const roundDuration = React.useMemo(
() => methods.reduce((acc, method) => acc + method.duration, 0),
[methods],
);
const totalDuration = React.useMemo(
() => roundDuration * repeat,
[roundDuration, repeat],
);
// ✅ Good: Use runOnJS for state updates
useAnimatedReaction(
() => Math.ceil(roundData.value.remainingTime),
(current, previous) => {
if (current !== previous) {
runOnJS(setRemainingTime)(current);
}
},
);
Performance Monitoring
const PerformanceMonitoredProvider = (props) => {
const renderCount = React.useRef(0);
const lastRenderTime = React.useRef(Date.now());
React.useEffect(() => {
renderCount.current++;
const now = Date.now();
const timeSinceLastRender = now - lastRenderTime.current;
if (timeSinceLastRender > 16) {
// More than 16ms (60fps threshold)
console.warn(`Slow render detected: ${timeSinceLastRender}ms`);
}
lastRenderTime.current = now;
});
return <BreathingExerciseProvider {...props} />;
};
Error Handling
Context Error Boundary
const BreathingExerciseErrorBoundary = ({ children, onError }) => {
return (
<ErrorBoundary FallbackComponent={BreathingFallback} onError={onError}>
{children}
</ErrorBoundary>
);
};
const BreathingFallback = ({ error, resetError }) => (
<View style={styles.errorContainer}>
<Text>Breathing exercise encountered an error</Text>
<Button onPress={resetError}>Try Again</Button>
</View>
);
Validation and Guards
const ValidatedBreathingProvider = ({
methods,
repeat,
children,
...props
}) => {
// Validate methods array
const isValidMethods = React.useMemo(() => {
return (
Array.isArray(methods) &&
methods.length === 3 &&
methods.every(
(method) =>
method &&
typeof method.duration === "number" &&
method.duration > 0 &&
["inhale", "hold", "exhale"].includes(method.type),
)
);
}, [methods]);
// Validate repeat count
const isValidRepeat = typeof repeat === "number" && repeat > 0;
if (!isValidMethods) {
console.error(
"Invalid methods array provided to BreathingExerciseProvider",
);
return <ErrorFallback error="Invalid breathing pattern" />;
}
if (!isValidRepeat) {
console.error("Invalid repeat count provided to BreathingExerciseProvider");
return <ErrorFallback error="Invalid repeat count" />;
}
return (
<BreathingExerciseProvider methods={methods} repeat={repeat} {...props}>
{children}
</BreathingExerciseProvider>
);
};
Testing
Provider Testing
import { renderHook } from "@testing-library/react-hooks";
import {
useBreathingExercise,
BreathingExerciseProvider,
} from "@breathing-exercise";
describe("BreathingExerciseProvider", () => {
const defaultProps = {
methods: [
{ type: "inhale", duration: 4 },
{ type: "hold", duration: 4 },
{ type: "exhale", duration: 4 },
],
repeat: 1,
};
const createWrapper =
(props = {}) =>
({ children }) => (
<BreathingExerciseProvider {...defaultProps} {...props}>
{children}
</BreathingExerciseProvider>
);
it("provides initial context values", () => {
const { result } = renderHook(() => useBreathingExercise(), {
wrapper: createWrapper(),
});
expect(result.current.methodType).toBe("inhale");
expect(result.current.remainingTime).toBe(4);
expect(result.current.progress).toBeDefined();
expect(result.current.roundData).toBeDefined();
});
it("calls onFinish when exercise completes", () => {
const onFinish = jest.fn();
const { result } = renderHook(() => useBreathingExercise(), {
wrapper: createWrapper({ onFinish }),
});
// Simulate exercise completion
act(() => {
result.current.progress.value = 1;
});
expect(onFinish).toHaveBeenCalled();
});
it("calculates total duration correctly", () => {
const { result } = renderHook(() => useBreathingExercise(), {
wrapper: createWrapper({ repeat: 3 }),
});
// Total duration should be (4 + 4 + 4) * 3 = 36 seconds
// This would be tested through the progress calculations
});
});
Integration Testing
import { render, act } from "@testing-library/react-native";
describe("BreathingExerciseProvider Integration", () => {
it("synchronizes state between multiple consumers", () => {
const TestConsumer1 = () => {
const { methodType } = useBreathingExercise();
return <Text testID="consumer1">{methodType}</Text>;
};
const TestConsumer2 = () => {
const { remainingTime } = useBreathingExercise();
return <Text testID="consumer2">{remainingTime}</Text>;
};
render(
<BreathingExerciseProvider {...defaultProps}>
<TestConsumer1 />
<TestConsumer2 />
</BreathingExerciseProvider>,
);
expect(screen.getByTestId("consumer1")).toHaveTextContent("inhale");
expect(screen.getByTestId("consumer2")).toHaveTextContent("4");
});
});
Common Patterns
Custom Hook Wrappers
const useBreathingProgress = () => {
const { progress, roundData } = useBreathingExercise();
return React.useMemo(
() => ({
overall: progress.value,
phase: roundData.value.animationProgress,
remaining: roundData.value.remainingTime,
}),
[progress.value, roundData.value],
);
};
const useBreathingPhase = () => {
const { methodType, remainingTime } = useBreathingExercise();
return {
current: methodType,
timeLeft: Math.ceil(remainingTime),
isInhaling: methodType === "inhale",
isHolding: methodType === "hold",
isExhaling: methodType === "exhale",
};
};
Conditional Rendering
const ConditionalBreathingComponents = () => {
const { methodType, progress } = useBreathingExercise();
return (
<View>
{methodType === "inhale" && <InhaleInstructions />}
{methodType === "hold" && <HoldInstructions />}
{methodType === "exhale" && <ExhaleInstructions />}
{progress.value > 0.5 && <HalfwayMessage />}
{progress.value > 0.9 && <AlmostDoneMessage />}
</View>
);
};
Best Practices
1. Provider Placement
// ✅ Good: Place provider close to consumers
const BreathingScreen = () => (
<BreathingExerciseProvider {...exerciseConfig}>
<BreathingUI />
</BreathingExerciseProvider>
);
// ❌ Bad: Provider too high in component tree
const App = () => (
<BreathingExerciseProvider {...exerciseConfig}>
<Navigation>
<SomeScreen>
<BreathingUI /> {/* Too far from provider */}
</SomeScreen>
</Navigation>
</BreathingExerciseProvider>
);
2. State Updates
// ✅ Good: Use shared values for animations
const AnimatedComponent = () => {
const { progress } = useBreathingExercise();
const animatedStyle = useAnimatedStyle(() => ({
opacity: progress.value,
}));
return <Animated.View style={animatedStyle} />;
};
// ❌ Bad: Using React state for frequent updates
const BadAnimatedComponent = () => {
const [opacity, setOpacity] = React.useState(0);
const { progress } = useBreathingExercise();
React.useEffect(() => {
setOpacity(progress.value); // Causes frequent re-renders
}, [progress.value]);
return <View style={{ opacity }} />;
};
3. Memory Management
// ✅ Good: Clean up resources
const BreathingSessionManager = () => {
React.useEffect(() => {
return () => {
// Cleanup when component unmounts
console.log("Cleaning up breathing session");
};
}, []);
return <BreathingExerciseProvider {...props} />;
};
The BreathingExerciseProvider forms the foundation of the breathing exercise system, providing reliable state management and precise timing control for creating engaging breathing experiences.