Skip to main content

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.