In our previous lesson, we built a BiometricGuard component that successfully integrated the @aparajita/capacitor-biometric-auth plugin. We implemented detailed availability checks and even made the component robust by handling app resume events. While functional, all of this logic is currently coupled directly to that single component.
Today, we'll take a significant step forward in terms of code architecture and reusability. We will refactor the logic from the BiometricGuard into a custom React hook, useBiometricAuth. This is a standard and powerful pattern in modern React development for encapsulating complex, stateful logic. By creating this hook, you'll be able to add biometric authentication capabilities to any component in your application with minimal boilerplate, promoting a clean and maintainable codebase.
1. The Custom Hook Pattern
As an experienced front-end developer, you're certainly familiar with the concept of custom hooks. They are functions that let us "hook into" React state and lifecycle features from function components. The goal here is to extract all the state management (useState), side effects (useEffect), and functions related to biometrics into a single, reusable unit.
Our useBiometricAuth hook will be responsible for:
- Checking for biometric availability on initial render.
- Keeping the availability status up-to-date when the app resumes.
- Exposing the type of biometry available (e.g., 'Face ID').
- Providing a simple function to trigger the authentication prompt.
- Managing loading and error states.
2. Building the useBiometricAuth Hook
Let's create the hook. A good starting point is an example implementation that shows the basic structure.
Antigravity × Capacitor Hybrid App Complete Guide: Converting ...
This guide from Antigravity provides a complete example of a useBiometricAuth hook. It's a great reference for the overall structure we want to achieve.
Focus on the code block for the file useBiometricAuth.ts. Notice how it uses useState and useEffect to manage and check for biometric availability, and exposes an authenticate function.
The example provides a solid foundation. We will now build upon it to create a more robust and feature-complete version that incorporates the advanced logic we developed in the last lesson, such as handling loading states, errors, and app resume events.
First, create a new file in your project: packages/web/src/hooks/useBiometricAuth.ts.
Now, let's write the full implementation. This version will be more comprehensive than the example, incorporating loading states, error handling, and the app resume listener.
// packages/web/src/hooks/useBiometricAuth.ts
import { useState, useEffect, useCallback } from 'react';
import { isPlatform } from '@ionic/react';
import {
BiometricAuth,
BiometryType,
CheckBiometryResult,
} from '@aparajita/capacitor-biometric-auth';
// The interface defining the state and functions our hook will return.
export interface BiometricAuthState {
isLoading: boolean;
isAvailable: boolean;
biometryType?: string;
error?: string;
authenticate: () => Promise<boolean>;
checkAvailability: () => Promise<void>;
}
// A helper function to get a user-friendly name for the biometry type.
const getBiometryName = (biometryType: BiometryType): string => {
switch (biometryType) {
case BiometryType.faceId:
return 'Face ID';
case BiometryType.touchId:
return 'Touch ID';
case BiometryType.fingerprintAuthentication:
return 'Fingerprint';
case BiometryType.faceAuthentication:
return 'Face Unlock';
case BiometryType.irisAuthentication:
return 'Iris Scan';
default:
return 'Biometrics';
}
};
export const useBiometricAuth = (): BiometricAuthState => {
const [isLoading, setIsLoading] = useState(true);
const [biometricInfo, setBiometricInfo] = useState<CheckBiometryResult | null>(null);
const [error, setError] = useState<string>();
// Use useCallback to memoize the check function
const checkAvailability = useCallback(async () => {
// On non-native platforms, we can consider biometrics 'unavailable' but not an error.
if (!isPlatform('capacitor')) {
setBiometricInfo({ isAvailable: false } as CheckBiometryResult);
setIsLoading(false);
return;
}
try {
setIsLoading(true);
const result = await BiometricAuth.checkBiometry();
setBiometricInfo(result);
if (!result.isAvailable) {
// Use the reason provided by the plugin if available.
setError(result.reason || 'Biometrics not available or not configured.');
} else {
setError(undefined); // Clear previous errors
}
} catch (e: any) {
setError(`Error checking biometric availability: ${e.message}`);
setBiometricInfo({ isAvailable: false } as CheckBiometryResult);
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
checkAvailability();
// Re-check availability when the app resumes from the background.
const listener = BiometricAuth.addResumeListener(async (result) => {
console.log('App resumed, re-checking biometrics...');
setBiometricInfo(result);
if (!result.isAvailable) {
setError(result.reason || 'Biometrics not available or not configured.');
} else {
setError(undefined);
}
});
// Cleanup: remove the listener when the hook is unmounted.
return () => {
listener.then(l => l.remove());
};
}, [checkAvailability]); // Depend on the memoized checkAvailability function
// Memoize the authenticate function as well.
const authenticate = useCallback(async (): Promise<boolean> => {
if (!biometricInfo?.isAvailable) {
console.error('Authentication called but biometrics not available.');
return false;
}
try {
await BiometricAuth.authenticate({
reason: 'For easy and secure access to your account',
// You can customize platform-specific options here
androidTitle: 'Authentication Required',
iosFallbackTitle: 'Use Passcode',
});
// The promise resolves on success
return true;
} catch (e: any) {
// The promise rejects on failure
console.error('Biometric authentication failed:', e);
setError(`Authentication failed: ${e.message || 'User cancelled'}`);
return false;
}
}, [biometricInfo]);
return {
isLoading,
isAvailable: biometricInfo?.isAvailable ?? false,
biometryType: biometricInfo?.biometryType ? getBiometryName(biometricInfo.biometryType) : undefined,
error,
authenticate,
checkAvailability,
};
};
Analysis of the Hook's Implementation:
- State Management: It uses
useStateto manageisLoading, the fullbiometricInfoobject, and anerrorstring. This provides consumers of the hook with a complete picture of the biometric status. checkAvailabilityFunction: We've wrapped the core logic in auseCallbackto prevent it from being recreated on every render. This function handles the initial check, updates loading and error states, and gracefully handles non-native environments.useEffectfor Lifecycle: The mainuseEffecthook runscheckAvailabilityon mount and, crucially, sets up theaddResumeListenerwe discussed in the last lesson. The cleanup function ensures the listener is removed when the component using the hook unmounts, preventing memory leaks.authenticateFunction: Thisasyncfunction, also wrapped inuseCallback, provides a simplePromise<boolean>interface. It abstracts away thetry...catchblock and plugin-specific options.- Return Value: The hook returns a clean object with everything a component needs:
isLoading,isAvailable,biometryType,error, and theauthenticatefunction itself.
You may want to refer back to the plugin's documentation for a refresher on the data structures and methods we're using here.
This documentation details the API we are abstracting with our hook.
Quickly review these key parts: The structure of the CheckBiometryResult interface to understand what biometricInfo holds. The purpose of addResumeListener. The behavior of the authenticate() method, especially its promise-based success/rejection flow.
3. Refactoring the BiometricGuard Component
Now for the payoff. Let's refactor our BiometricGuard.tsx component to use the new useBiometricAuth hook. You will see how dramatically this simplifies the component's code.
// packages/web/src/components/BiometricGuard.tsx (Refactored)
import React, { useState, useEffect } from 'react';
import { useBiometricAuth } from '../hooks/useBiometricAuth'; // Import our new hook
interface BiometricGuardProps {
children: React.ReactNode;
}
const BiometricGuard: React.FC<BiometricGuardProps> = ({ children }) => {
// All complex logic is now in the hook!
const { isLoading, isAvailable, biometryType, error, authenticate } = useBiometricAuth();
const [isAuthenticated, setIsAuthenticated] = useState(false);
// A single effect to handle authentication once availability is confirmed.
useEffect(() => {
// Automatically try to authenticate once we know it's available.
if (isAvailable && !isAuthenticated) {
handleAuthentication();
}
}, [isAvailable, isAuthenticated]); // Re-run if availability changes.
const handleAuthentication = async () => {
const success = await authenticate();
setIsAuthenticated(success);
};
if (isLoading) {
return <div>Checking biometric capabilities...</div>;
}
if (isAuthenticated) {
return <>{children}</>;
}
// Fallback UI if not authenticated
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>Authentication Required</h2>
<p>Please verify your identity to access this content.</p>
{isAvailable ? (
<button onClick={handleAuthentication}>
Authenticate with {biometryType || 'Biometrics'}
</button>
) : (
<p style={{ color: 'orange', marginTop: '10px' }}>
Biometric authentication is not available on this device.
</p>
)}
{error && !isAvailable && <p style={{ color: 'red', marginTop: '10px' }}>{error}</p>}
</div>
);
};
export default BiometricGuard;
Look at how clean the BiometricGuard component has become. It's now purely a presentational component concerned with UI states (loading, authenticated, error). All the heavy lifting of interacting with the Capacitor plugin is handled by useBiometricAuth. This separation of concerns is a hallmark of robust front-end architecture.
Conclusion
In this lesson, you've successfully applied a key React pattern to our Capacitor project. By creating the useBiometricAuth custom hook, you've built a reusable, maintainable, and easy-to-use API for handling biometrics.
Key takeaways from this session include:
- Abstraction is Power: Custom hooks are the idiomatic way in React to abstract complex, stateful logic away from your components.
- A Robust Hook's Anatomy: A good hook manages not just the happy path, but also loading states, error conditions, and lifecycle events like app resume.
- Improved Component Quality: Components that use well-designed hooks become simpler, more declarative, and easier to test, as their primary responsibility shifts to rendering UI based on the state provided by the hook.
With this powerful useBiometricAuth hook in your toolkit, you are now perfectly positioned for the next step. In our next lesson, we will use this hook to implement route-level security, gating access to entire sections of your application until a user has successfully authenticated.
Can't find a good explanation? Sign up and we'll make it for you
Sign up