Skip to main content

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 },
];
PropTypeRequiredDescription
childrenReact.ReactNodeChild components that will consume the context
methodsBreathingPatternArray defining inhale, hold, and exhale durations
repeatnumberNumber of complete breathing cycles
onFinish() => voidCallback 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

  1. Initialization: Provider sets up shared values and initial state
  2. Progress Updates: Shared progress value drives all calculations
  3. Phase Detection: roundData determines current breathing phase
  4. State Synchronization: React state updates when phase changes
  5. Completion: onFinish callback 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.