useBreathingExercise Hook
The useBreathingExercise hook provides access to the breathing exercise context, offering real-time state, progress tracking, and timing information. It serves as the primary interface for consuming breathing exercise data within components.
Overview
useBreathingExercise is a custom React hook that accesses the BreathingExerciseContext and provides a clean, type-safe interface for consuming breathing exercise state. It ensures that components are properly wrapped with BreathingExerciseProvider and offers comprehensive access to all exercise-related data.
Features
- 🎯 Context Access: Direct access to breathing exercise state
- 📊 Real-time Data: Live progress and timing updates
- 🔒 Type Safety: Fully typed return values with TypeScript
- ⚡ Performance Optimized: Minimal re-renders with shared values
- 🛡️ Error Prevention: Automatic validation of provider presence
- 🧩 Composable: Works seamlessly with all breathing exercise components
- 📱 Developer Experience: Clear error messages and intuitive API
API Reference
Hook Signature
function useBreathingExercise(): BreathingExerciseContextType;
Return Value
interface BreathingExerciseContextType {
// Configuration (from props)
methods: BreathingPattern;
repeat: number;
onFinish?: () => void;
// Real-time State
progress: SharedValue<number>; // Overall exercise progress (0-1)
roundData: SharedValue<{
method: BreathingPattern[number]; // Current breathing method
animationProgress: number; // Animation progress (0-1)
remainingTime: number; // Time left in current phase
}>;
remainingTime: number; // React state for remaining time
methodType: "inhale" | "hold" | "exhale"; // Current breathing phase
}
type BreathingPattern = [
{ type: "inhale"; duration: number },
{ type: "hold"; duration: number },
{ type: "exhale"; duration: number },
];
Basic Usage
Simple State Access
import { useBreathingExercise } from "@breathing-exercise";
const BreathingDisplay = () => {
const { methodType, remainingTime, progress } = useBreathingExercise();
return (
<View style={styles.container}>
<Text style={styles.phase}>Current Phase: {methodType}</Text>
<Text style={styles.time}>
Time Remaining: {Math.ceil(remainingTime)}s
</Text>
<Text style={styles.progress}>
Progress: {Math.round(progress.value * 100)}%
</Text>
</View>
);
};
Progress Visualization
import { useAnimatedStyle } from "react-native-reanimated";
const ProgressBar = () => {
const { progress } = useBreathingExercise();
const progressStyle = useAnimatedStyle(() => ({
width: `${progress.value * 100}%`,
}));
return (
<View style={styles.progressContainer}>
<Animated.View style={[styles.progressFill, progressStyle]} />
</View>
);
};
Phase-Specific Rendering
const PhaseSpecificContent = () => {
const { methodType, remainingTime } = useBreathingExercise();
const getPhaseInstruction = () => {
switch (methodType) {
case "inhale":
return "Breathe in slowly and deeply";
case "hold":
return "Hold your breath";
case "exhale":
return "Breathe out slowly and completely";
default:
return "";
}
};
const getPhaseColor = () => {
const colors = {
inhale: "#4CAF50",
hold: "#FF9800",
exhale: "#2196F3",
};
return colors[methodType] || "#9E9E9E";
};
return (
<View style={[styles.phaseContainer, { backgroundColor: getPhaseColor() }]}>
<Text style={styles.instruction}>{getPhaseInstruction()}</Text>
<Text style={styles.countdown}>{Math.ceil(remainingTime)}</Text>
</View>
);
};
Advanced Usage
Animation Synchronization
import Animated, {
useAnimatedStyle,
interpolate,
} from "react-native-reanimated";
const SynchronizedBreathingCircle = () => {
const { roundData, progress } = useBreathingExercise();
// Circle scale animation based on breathing phase
const circleStyle = useAnimatedStyle(() => {
const scale = interpolate(
roundData.value.animationProgress,
[0, 0.33, 0.66, 1],
[0.8, 1.2, 1.2, 0.8], // Grow on inhale, maintain on hold, shrink on exhale
);
return {
transform: [{ scale }],
};
});
// Background color animation
const backgroundStyle = useAnimatedStyle(() => {
const method = roundData.value.method.type;
const colors = {
inhale: "rgba(76, 175, 80, 0.2)",
hold: "rgba(255, 152, 0, 0.2)",
exhale: "rgba(33, 150, 243, 0.2)",
};
return {
backgroundColor: colors[method] || "rgba(158, 158, 158, 0.2)",
};
});
// Progress ring rotation
const ringStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${progress.value * 360}deg` }],
}));
return (
<Animated.View style={[styles.container, backgroundStyle]}>
<Animated.View style={[styles.progressRing, ringStyle]} />
<Animated.View style={[styles.breathingCircle, circleStyle]} />
</Animated.View>
);
};
Custom Progress Tracking
const useBreathingProgress = () => {
const { progress, roundData, repeat } = useBreathingExercise();
const progressData = React.useMemo(() => {
const overallProgress = progress.value;
const currentRound = Math.floor(overallProgress * repeat) + 1;
const roundProgress = (overallProgress * repeat) % 1;
return {
overall: overallProgress,
currentRound: Math.min(currentRound, repeat),
roundProgress,
phase: roundData.value.animationProgress,
isLastRound: currentRound === repeat,
};
}, [progress.value, roundData.value, repeat]);
return progressData;
};
const DetailedProgressDisplay = () => {
const progressData = useBreathingProgress();
return (
<View style={styles.progressDetails}>
<Text>
Round: {progressData.currentRound} / {progressData.repeat}
</Text>
<Text>
Round Progress: {Math.round(progressData.roundProgress * 100)}%
</Text>
<Text>Overall Progress: {Math.round(progressData.overall * 100)}%</Text>
{progressData.isLastRound && (
<Text style={styles.lastRoundText}>Final Round!</Text>
)}
</View>
);
};
Session Analytics
const useBreathingAnalytics = () => {
const { methodType, progress, remainingTime } = useBreathingExercise();
const [analytics, setAnalytics] = React.useState({
startTime: Date.now(),
phaseChanges: 0,
totalPhaseTime: { inhale: 0, hold: 0, exhale: 0 },
currentPhaseStartTime: Date.now(),
});
// Track phase changes
const previousPhase = React.useRef(methodType);
React.useEffect(() => {
if (previousPhase.current !== methodType) {
const now = Date.now();
const phaseTime = now - analytics.currentPhaseStartTime;
setAnalytics((prev) => ({
...prev,
phaseChanges: prev.phaseChanges + 1,
totalPhaseTime: {
...prev.totalPhaseTime,
[previousPhase.current]:
prev.totalPhaseTime[previousPhase.current] + phaseTime,
},
currentPhaseStartTime: now,
}));
previousPhase.current = methodType;
}
}, [methodType, analytics.currentPhaseStartTime]);
const getSessionSummary = React.useCallback(() => {
const sessionDuration = Date.now() - analytics.startTime;
const completionRate = progress.value;
return {
duration: sessionDuration,
completionRate,
phaseChanges: analytics.phaseChanges,
averagePhaseTime: analytics.totalPhaseTime,
};
}, [analytics, progress.value]);
return { analytics, getSessionSummary };
};
const AnalyticsDisplay = () => {
const { analytics, getSessionSummary } = useBreathingAnalytics();
return (
<View style={styles.analyticsContainer}>
<Text>Phase Changes: {analytics.phaseChanges}</Text>
<Text>
Session Duration:{" "}
{Math.floor((Date.now() - analytics.startTime) / 1000)}s
</Text>
</View>
);
};
Conditional Hooks Pattern
const useBreathingPhaseEffects = () => {
const { methodType, remainingTime } = useBreathingExercise();
// Haptic feedback on phase changes
const previousPhase = React.useRef(methodType);
React.useEffect(() => {
if (previousPhase.current !== methodType) {
// Trigger haptic feedback
if (typeof navigator !== "undefined" && navigator.vibrate) {
navigator.vibrate(100);
}
previousPhase.current = methodType;
}
}, [methodType]);
// Audio cues
React.useEffect(() => {
if (remainingTime <= 1 && remainingTime > 0) {
// Play countdown sound
console.log("Countdown:", Math.ceil(remainingTime));
}
}, [remainingTime]);
// Phase-specific side effects
React.useEffect(() => {
switch (methodType) {
case "inhale":
// Start gentle background music
break;
case "hold":
// Pause background music
break;
case "exhale":
// Resume background music
break;
}
return () => {
// Cleanup phase-specific effects
};
}, [methodType]);
};
Integration Patterns
Component Composition
const BreathingExerciseCard = () => {
const { methodType, remainingTime, progress } = useBreathingExercise();
return (
<Card style={styles.card}>
<Card.Content>
<PhaseIcon type={methodType} />
<PhaseName type={methodType} />
<TimeDisplay time={remainingTime} />
<ProgressIndicator progress={progress.value} />
</Card.Content>
</Card>
);
};
const PhaseIcon = ({ type }) => {
const iconName = {
inhale: "arrow-up",
hold: "pause",
exhale: "arrow-down",
}[type];
return <Icon name={iconName} size={24} />;
};
const PhaseName = ({ type }) => {
const { t } = useTranslation();
return <Text style={styles.phaseName}>{t(`breathing.${type}`)}</Text>;
};
State-Based Navigation
const useBreathingNavigation = () => {
const { progress, methodType } = useBreathingExercise();
const navigation = useNavigation();
React.useEffect(() => {
if (progress.value >= 1) {
// Navigate to completion screen
navigation.navigate("BreathingComplete");
}
}, [progress.value, navigation]);
React.useEffect(() => {
// Update navigation header based on current phase
navigation.setOptions({
title: `Breathing Exercise - ${methodType}`,
});
}, [methodType, navigation]);
};
Error Handling
Hook Validation
The hook automatically validates that it's used within a BreathingExerciseProvider:
export const useBreathingExercise = () => {
const context = React.useContext(BreathingExerciseContext);
if (!context) {
throw new Error(
"useBreathingExercise must be used within a BreathingExerciseProvider",
);
}
return context;
};
Safe Hook Usage
const SafeBreathingComponent = () => {
try {
const exerciseData = useBreathingExercise();
return <BreathingDisplay data={exerciseData} />;
} catch (error) {
console.error("Breathing exercise context error:", error);
return <ErrorFallback />;
}
};
// Or using error boundary
const BreathingWithErrorBoundary = () => (
<ErrorBoundary fallback={<ErrorFallback />}>
<BreathingComponent />
</ErrorBoundary>
);
Custom Error Hook
const useSafeBreathingExercise = () => {
const [error, setError] = React.useState(null);
try {
const context = useBreathingExercise();
return { context, error: null };
} catch (err) {
React.useEffect(() => {
setError(err);
}, [err]);
return { context: null, error };
}
};
Performance Considerations
Selective State Access
// ✅ Good: Only access needed values
const MinimalComponent = () => {
const { methodType } = useBreathingExercise();
return <Text>{methodType}</Text>;
};
// ❌ Bad: Destructuring all values
const BadComponent = () => {
const {
methodType,
remainingTime,
progress,
roundData,
methods,
repeat,
onFinish,
} = useBreathingExercise();
return <Text>{methodType}</Text>; // Only using methodType
};
Memoization Strategies
const OptimizedBreathingDisplay = () => {
const { methodType, remainingTime } = useBreathingExercise();
// Memoize expensive calculations
const formattedTime = React.useMemo(() => {
const minutes = Math.floor(remainingTime / 60);
const seconds = Math.ceil(remainingTime % 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}, [remainingTime]);
// Memoize component to prevent unnecessary re-renders
return React.memo(() => (
<View>
<Text>{methodType}</Text>
<Text>{formattedTime}</Text>
</View>
));
};
Animation Performance
const PerformantAnimatedComponent = () => {
const { roundData } = useBreathingExercise();
// Use shared values directly in animations
const animatedStyle = useAnimatedStyle(
() => ({
opacity: roundData.value.animationProgress,
transform: [
{
scale: 1 + roundData.value.animationProgress * 0.2,
},
],
}),
[],
); // Empty dependency array - shared values handle updates
return <Animated.View style={animatedStyle} />;
};
Testing
Hook Testing
import { renderHook } from "@testing-library/react-hooks";
import {
useBreathingExercise,
BreathingExerciseProvider,
} from "@breathing-exercise";
describe("useBreathingExercise", () => {
const defaultProps = {
methods: [
{ type: "inhale", duration: 4 },
{ type: "hold", duration: 4 },
{ type: "exhale", duration: 4 },
],
repeat: 1,
};
const wrapper = ({ children }) => (
<BreathingExerciseProvider {...defaultProps}>
{children}
</BreathingExerciseProvider>
);
it("returns breathing exercise context", () => {
const { result } = renderHook(() => useBreathingExercise(), { wrapper });
expect(result.current.methodType).toBe("inhale");
expect(result.current.remainingTime).toBe(4);
expect(result.current.progress).toBeDefined();
expect(result.current.roundData).toBeDefined();
expect(result.current.methods).toEqual(defaultProps.methods);
expect(result.current.repeat).toBe(1);
});
it("throws error when used outside provider", () => {
const { result } = renderHook(() => useBreathingExercise());
expect(result.error).toEqual(
Error(
"useBreathingExercise must be used within a BreathingExerciseProvider",
),
);
});
it("provides real-time updates", () => {
const { result } = renderHook(() => useBreathingExercise(), { wrapper });
// Test that shared values are reactive
expect(typeof result.current.progress.value).toBe("number");
expect(typeof result.current.roundData.value).toBe("object");
});
});
Component Integration Testing
import { render, screen } from "@testing-library/react-native";
describe("useBreathingExercise Integration", () => {
const TestComponent = () => {
const { methodType, remainingTime } = useBreathingExercise();
return (
<View>
<Text testID="method-type">{methodType}</Text>
<Text testID="remaining-time">{remainingTime}</Text>
</View>
);
};
it("integrates with provider correctly", () => {
render(
<BreathingExerciseProvider {...defaultProps}>
<TestComponent />
</BreathingExerciseProvider>,
);
expect(screen.getByTestId("method-type")).toHaveTextContent("inhale");
expect(screen.getByTestId("remaining-time")).toHaveTextContent("4");
});
});
Common Patterns
Custom Hook Extensions
const useBreathingPhase = () => {
const { methodType } = useBreathingExercise();
return {
current: methodType,
isInhaling: methodType === 'inhale',
isHolding: methodType === 'hold',
isExhaling: methodType === 'exhale',
};
};
const useBreathingTimer = () => {
const { remainingTime, progress } = useBreathingExercise();
return {
remaining: Math.ceil(remainingTime),
elapsed: /* calculate elapsed time */,
percentage: progress.value * 100,
isNearEnd: remainingTime < 5,
};
};
Conditional Content
const ConditionalBreathingContent = () => {
const { isInhaling, isHolding, isExhaling } = useBreathingPhase();
return (
<View>
{isInhaling && <InhaleInstructions />}
{isHolding && <HoldInstructions />}
{isExhaling && <ExhaleInstructions />}
</View>
);
};
Best Practices
1. Selective State Access
// ✅ Good: Only destructure what you need
const Component = () => {
const { methodType } = useBreathingExercise();
return <PhaseDisplay phase={methodType} />;
};
2. Shared Value Usage
// ✅ Good: Use shared values for animations
const AnimatedComponent = () => {
const { progress } = useBreathingExercise();
const style = useAnimatedStyle(() => ({
opacity: progress.value,
}));
return <Animated.View style={style} />;
};
3. Error Handling
// ✅ Good: Wrap in error boundary
const SafeBreathingComponent = () => (
<ErrorBoundary fallback={<ErrorFallback />}>
<BreathingComponent />
</ErrorBoundary>
);
4. Performance Optimization
// ✅ Good: Memoize expensive operations
const OptimizedComponent = () => {
const { methodType, remainingTime } = useBreathingExercise();
const displayData = React.useMemo(
() => ({
phase: methodType.toUpperCase(),
time: formatTime(remainingTime),
}),
[methodType, remainingTime],
);
return <Display data={displayData} />;
};
The useBreathingExercise hook provides a powerful and flexible interface for accessing breathing exercise state, enabling developers to create rich, interactive breathing experiences with minimal complexity and maximum performance.