# React Hooks for react-native-sensitive-info
This document covers the React hooks API for `react-native-sensitive-info`, designed with modern React best practices including automatic cleanup, memory leak prevention, and performance optimization.
## Table of Contents
- [Quick Start](#quick-start)
- [Core Hooks](#core-hooks)
- [Best Practices](#best-practices)
- [Performance Considerations](#performance-considerations)
- [Error Handling](#error-handling)
- [Migration Guide](#migration-guide)
- [Examples](#examples)
## Quick Start
### Installation
```bash
npm install react-native-sensitive-info
# or
yarn add react-native-sensitive-info
```
### Basic Usage
```tsx
import { useSecretItem, useSecureStorage } from 'react-native-sensitive-info/hooks'
function MyComponent() {
// Read a single secret
const { data, isLoading, error } = useSecretItem('apiToken')
// Manage all secrets in a service
const { items, saveSecret, removeSecret } = useSecureStorage({
service: 'myapp'
})
if (isLoading) return Loading...
if (error) return Error: {error.message}
return {data?.value}
}
```
## Core Hooks
### `useSecretItem`
Fetches and manages a single secure storage item with automatic loading and error states.
#### API
```typescript
function useSecretItem(
key: string,
options?: SensitiveInfoOptions & {
includeValue?: boolean
skip?: boolean
}
): AsyncState & {
refetch: () => Promise
}
interface AsyncState {
data: TData | null
error: HookError | null
isLoading: boolean
isPending: boolean
}
```
#### Features
- ✅ Automatic request cancellation on unmount
- ✅ Memory leak prevention via cleanup
- ✅ Conditional loading with `skip` parameter
- ✅ Manual refetch support
- ✅ Type-safe error handling
#### Example
```tsx
function TokenViewer() {
const { data, isLoading, error, refetch } = useSecretItem('refreshToken', {
service: 'auth',
accessControl: 'secureEnclaveBiometry',
authenticationPrompt: {
title: 'Authenticate',
description: 'Required to access your token'
}
})
if (isLoading) return
if (error) return Failed to load token: {error.message}
if (!data) return No token found
return (
{data.value}
)
}
```
---
### `useSecret`
A convenience hook that combines reading and writing a single secret. Includes save and delete operations.
#### API
```typescript
function useSecret(
key: string,
options?: SensitiveInfoOptions & { includeValue?: boolean }
): AsyncState & {
saveSecret: (value: string) => Promise<{ success: boolean; error?: HookError }>
deleteSecret: () => Promise<{ success: boolean; error?: HookError }>
refetch: () => Promise
}
```
#### Features
- ✅ Read and write in a single hook
- ✅ Automatic state synchronization after mutations
- ✅ Optimized for single secret management
#### Example
```tsx
function AuthTokenManager() {
const {
data: token,
isLoading,
saveSecret,
deleteSecret,
refetch
} = useSecret('authToken', { service: 'myapp' })
const handleLogout = async () => {
const { success, error } = await deleteSecret()
if (success) {
navigateTo('Login')
} else {
showError(error?.message)
}
}
const handleRefreshToken = async (newToken: string) => {
const { success } = await saveSecret(newToken)
if (success) {
showNotification('Token updated')
await refetch()
}
}
return (
{token && Token exists: {token.metadata.securityLevel}}
)
}
```
---
### `useHasSecret`
Lightweight hook for checking if a secret exists without fetching its value.
#### API
```typescript
function useHasSecret(
key: string,
options?: SensitiveInfoOptions & { skip?: boolean }
): AsyncState & {
refetch: () => Promise
}
```
#### Features
- ✅ Efficient existence checks
- ✅ Minimal performance overhead
- ✅ No decryption needed
#### Example
```tsx
function ConditionalContent() {
const { data: tokenExists, isLoading } = useHasSecret('apiToken')
if (isLoading) return Checking...
return tokenExists ? :
}
```
---
### `useSecureStorage`
Manages all secrets in a service with full CRUD operations and automatic state synchronization.
#### API
```typescript
function useSecureStorage(
options?: SensitiveInfoOptions & {
includeValues?: boolean
skip?: boolean
}
): {
items: SensitiveInfoItem[]
isLoading: boolean
error: HookError | null
saveSecret: (key: string, value: string) => Promise<{ success: boolean; error?: HookError }>
removeSecret: (key: string) => Promise<{ success: boolean; error?: HookError }>
clearAll: () => Promise<{ success: boolean; error?: HookError }>
refreshItems: () => Promise
}
```
#### Features
- ✅ Full CRUD operations
- ✅ Optimistic updates for delete
- ✅ Automatic list refresh after save/delete
- ✅ Selective value inclusion
- ✅ Service-wide operations
#### Example
```tsx
function SecureStorageManager() {
const {
items,
isLoading,
error,
saveSecret,
removeSecret,
clearAll,
refreshItems
} = useSecureStorage({
service: 'credentials',
includeValues: false // Don't fetch values initially
})
const handleAddSecret = async () => {
const { success, error: err } = await saveSecret('apiKey', 'secret-value')
if (!success) {
showError(err?.message)
}
}
const handleRemoveSecret = async (key: string) => {
const { success } = await removeSecret(key)
if (success) {
showNotification(`Deleted ${key}`)
}
}
const handleClearAll = async () => {
if (confirm('Delete all secrets?')) {
const { success } = await clearAll()
if (success) {
showNotification('All secrets cleared')
}
}
}
if (isLoading) return
if (error) return Error: {error.message}
return (
(
handleRemoveSecret(item.key)}
/>
)}
keyExtractor={item => item.key}
/>
)
}
```
---
### `useSecurityAvailability`
Fetches and caches device security capabilities (Secure Enclave, StrongBox, Biometry, etc.).
#### API
```typescript
function useSecurityAvailability(
options?: UseSecurityAvailabilityOptions
): AsyncState & {
refetch: () => Promise
}
interface UseSecurityAvailabilityOptions {
/** Auto-refresh when the app returns to `active`. Debounced ~500 ms. */
readonly refreshOnForeground?: boolean
}
interface SecurityAvailability {
readonly secureEnclave: boolean
readonly strongBox: boolean
readonly biometry: boolean
readonly biometryStatus:
| 'available'
| 'notEnrolled'
| 'notAvailable'
| 'lockedOut'
| 'unknown'
readonly deviceCredential: boolean
}
```
#### Features
- ✅ Result cached **per component instance** — no native call on re-render
- ✅ `refetch()` available to bypass the cache after settings changes
- ✅ Previous data preserved on error
- ✅ `biometryStatus` distinguishes *no hardware* from *hardware present but unenrolled* — drive an *“Enroll Face ID”* CTA off `'notEnrolled'` instead of hiding the toggle
- ✅ `refreshOnForeground` subscribes to `AppState` and refetches when the user returns from system settings (off by default)
#### Example
```tsx
function AccessControlSelector() {
const { data: capabilities, isLoading } = useSecurityAvailability({
refreshOnForeground: true,
})
if (isLoading) return Detecting capabilities...
if (capabilities?.biometryStatus === 'notEnrolled') {
return (
Linking.openSettings()}>
Set up Face ID / fingerprint →
)
}
return (
{capabilities?.secureEnclave && ✓ Secure Enclave available}
{capabilities?.biometry && ✓ Biometry available}
{capabilities?.deviceCredential && ✓ Device credential available}
)
}
```
#### React to enrollment changes
Use `useBiometryStatusWatcher` for transition-only callbacks (fires once per real `BiometryStatus` change, never on every render):
```tsx
import { useBiometryStatusWatcher } from 'react-native-sensitive-info/hooks'
useBiometryStatusWatcher((next, previous) => {
if (previous === 'notEnrolled' && next === 'available') {
showToast('Face ID is ready.')
}
})
```
#### Gate writes on a specific access-control policy
Pair the snapshot with `canUseAccessControlSync` so the toggle reflects whether the policy you intend to use will actually succeed:
```tsx
import { canUseAccessControlSync } from 'react-native-sensitive-info'
const { data: caps } = useSecurityAvailability()
const canEnableSecureEnclave = caps
? canUseAccessControlSync('secureEnclaveBiometry', caps)
: false
```
---
### `useKeyRotation`
Manage versioned master-key rotation for a given service. Calls `rotateKeys()` under the hood and keeps the active version, last rotation result, and loading/error state.
#### API
```typescript
function useKeyRotation(options?: UseKeyRotationOptions): {
lastResult: RotationResult | null
error: HookError | null
isRotating: boolean
rotate: () => Promise
readVersion: () => Promise
}
interface UseKeyRotationOptions extends SensitiveInfoOptions {
reEncryptEagerly?: boolean // default: false (lazy rotation)
}
interface RotationResult {
previousVersion: number
newVersion: number
reEncryptedCount: number
}
```
#### Example
```tsx
import { useKeyRotation } from 'react-native-sensitive-info/hooks'
function RotationButton() {
const { rotate, isRotating, lastResult, error } = useKeyRotation({
service: 'auth',
})
return (
{lastResult && (
v{lastResult.previousVersion} → v{lastResult.newVersion}
)}
{error && {error.message}}
)
}
```
> **Note:** Defaults to lazy rotation — entries are re-encrypted opportunistically when they are next read. Pass `reEncryptEagerly: true` to walk every entry up front.
---
### `useSecureOperation`
One-time operation hook for non-reactive operations (e.g., bulk operations, logout).
#### API
```typescript
function useSecureOperation(): VoidAsyncState & {
execute: (operation: () => Promise) => Promise
}
interface VoidAsyncState {
error: HookError | null
isLoading: boolean
isPending: boolean
}
```
#### Features
- ✅ Flexible operation execution
- ✅ Loading state management
- ✅ Error handling
#### Example
```tsx
function LogoutButton() {
const { execute, isLoading, error } = useSecureOperation()
const handleLogout = async () => {
await execute(async () => {
// Clear all app credentials
await clearService({ service: 'auth' })
await clearService({ service: 'cache' })
// Navigate to login
navigateTo('Login')
})
}
if (error) return Logout failed: {error.message}
return (
)
}
```
---
## No Setup Required
All hooks work independently without any provider. Just import and use them directly in your components:
```tsx
import {
useSecureStorage,
useSecurityAvailability,
} from 'react-native-sensitive-info/hooks'
function MyComponent() {
const { items } = useSecureStorage({ service: 'myapp' })
const { data: capabilities } = useSecurityAvailability()
// Each hook instance keeps its own cache. Mounting `useSecurityAvailability`
// in two components issues two native reads (one per instance), but neither
// re-runs across re-renders unless you call `refetch()`.
}
```
---
## Best Practices
### 1. Memory Leak Prevention ✅
All hooks automatically clean up resources on unmount:
```tsx
// ✅ GOOD: Automatic cleanup
function Component() {
const { data, isLoading } = useSecretItem('token')
// Cleanup happens automatically on unmount
}
```
### 2. Avoid Unnecessary Re-renders
Use the `skip` parameter to conditionally skip fetches:
```tsx
// ✅ GOOD: Conditional fetching
function Component() {
const isAuthenticated = useIsAuthenticated()
const { data } = useSecretItem('token', { skip: !isAuthenticated })
// Won't fetch until user is authenticated
}
```
### 3. Use `useMemo` for Options
Stabilize options objects to prevent unnecessary API calls:
```tsx
// ✅ GOOD: Memoized options
const options = useMemo(() => ({
service: 'myapp',
accessControl: 'secureEnclaveBiometry'
}), []) // Empty deps - only create once
const { data } = useSecretItem('token', options)
// ❌ BAD: New object every render
const { data } = useSecretItem('token', {
service: 'myapp',
accessControl: 'secureEnclaveBiometry'
})
```
### 4. Handle Errors Gracefully
Always check error states and provide user feedback:
```tsx
// ✅ GOOD: Proper error handling
function Component() {
const { data, error, isLoading } = useSecretItem('token')
if (isLoading) return
if (error) return
if (!data) return No data found
return {data.value}
}
```
### 5. Batch Operations
Use `useSecureStorage` instead of multiple `useSecretItem` calls:
```tsx
// ✅ GOOD: Single hook for multiple items
function Component() {
const { items } = useSecureStorage({ service: 'auth' })
// Access all items
}
// ❌ AVOID: Multiple hook instances
const token = useSecretItem('token')
const refresh = useSecretItem('refreshToken')
const apiKey = useSecretItem('apiKey')
```
### 6. Capability Caching Is Per-Instance
Each `useSecurityAvailability` mount keeps its own cache, so re-renders never trigger a fresh
native call. Multiple components mounting the hook will each issue one read — if you need a
single source of truth, lift the hook into a parent and pass `data` down via props.
```tsx
// ✅ Re-renders are free — first mount caches, subsequent renders reuse the value.
function Capabilities() {
const { data, isLoading, refetch } = useSecurityAvailability()
// Call refetch() after the user changes biometric enrollment in system settings.
}
```
### 7. Accessing Security Capabilities
Check what security features are available on the device:
```tsx
// ✅ GOOD: Direct hook usage
function SecurityStatus() {
const { data: capabilities, isLoading } = useSecurityAvailability()
if (isLoading) return
return (
Biometric: {capabilities?.isBiometricEnabled ? '✓' : '✗'}
Strong Box: {capabilities?.isStrongBoxAvailable ? '✓' : '✗'}
)
}
```
### 8. Refetch Data Strategically
Use `refetch()` when you need to sync state with native storage:
```tsx
// ✅ GOOD: Manual refetch after external updates
const { data, refetch } = useSecretItem('token')
const handleExternalUpdate = async () => {
await externallyUpdateToken()
await refetch() // Sync with native state
}
```
---
## Performance Considerations
### 1. Request Cancellation
All hooks automatically cancel in-flight requests on unmount:
```tsx
// If component unmounts while fetching, request is cancelled
const { data, isLoading } = useSecretItem('token')
```
### 2. Caching
`useSecurityAvailability` caches results to avoid repeated native calls:
```tsx
const cap1 = useSecurityAvailability() // Calls native
const cap2 = useSecurityAvailability() // Uses cache
```
### 3. Selective Value Fetching
Use `includeValues: false` when you only need metadata:
```tsx
// ✅ GOOD: Only fetch metadata
const { items } = useSecureStorage({ includeValues: false })
// ❌ AVOID: Unnecessary decryption
const { items } = useSecureStorage({ includeValues: true })
```
### 4. Optimistic Updates
Delete operations update UI immediately:
```tsx
const { removeSecret } = useSecureStorage()
// UI updates immediately, native call happens in background
await removeSecret('token') // Optimistic delete
```
---
## Error Handling
### Understanding Errors
The `HookError` class wraps errors with context:
```typescript
class HookError extends Error {
constructor(
message: string,
public readonly originalError?: unknown
) {}
}
```
### Error Handling Patterns
```tsx
function Component() {
const { error, data } = useSecretItem('token')
if (error) {
// Log original error for debugging
console.error('Hook error:', error.originalError)
// Show user-friendly message
return Failed to load token: {error.message}
}
return {data?.value}
}
```
---
## Migration Guide
### From Callback-Based API to Hooks
#### Before (Callback API)
```tsx
function Component() {
const [token, setToken] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let mounted = true
const fetchToken = async () => {
try {
const item = await getItem('token')
if (mounted) setToken(item)
} catch (err) {
if (mounted) setError(err)
} finally {
if (mounted) setLoading(false)
}
}
fetchToken()
return () => {
mounted = false
}
}, [])
return loading ? Loading : {token?.value}
}
```
#### After (Hooks API)
```tsx
// ✅ MUCH CLEANER
function Component() {
const { data: token, isLoading, error } = useSecretItem('token')
return isLoading ? Loading : {token?.value}
}
```
---
## Examples
### Complete Authentication Flow
```tsx
import {
useSecret,
useSecurityAvailability,
} from 'react-native-sensitive-info/hooks'
function AuthenticationFlow() {
const {
data: token,
isLoading: tokenLoading,
saveSecret,
deleteSecret
} = useSecret('authToken', {
service: 'myapp',
accessControl: 'secureEnclaveBiometry'
})
const { data: capabilities } = useSecurityAvailability()
const handleLogin = async (credentials) => {
const response = await login(credentials)
const { success } = await saveSecret(response.token)
if (success) {
navigateTo('Home')
}
}
const handleLogout = async () => {
const { success } = await deleteSecret()
if (success) {
navigateTo('Login')
}
}
return token ? :
}
```
### Biometric Authentication
```tsx
function BiometricAuth() {
const { data: capabilities } = useSecurityAvailability()
const { data: storedToken } = useSecretItem('biometricToken')
const canUseBiometry = capabilities?.biometry ?? false
if (!canUseBiometry) {
return Biometry not available
}
return (
{
const item = await getItem('biometricToken', {
authenticationPrompt: {
title: 'Authenticate',
description: 'Use your biometry to unlock'
}
})
if (item) {
authorizeUser(item.value)
}
}}
/>
)
}
```
### Multi-Service Management
```tsx
function CredentialsManager() {
const authCredentials = useSecureStorage({
service: 'auth',
includeValues: false
})
const apiKeys = useSecureStorage({
service: 'api',
includeValues: false
})
return (
{authCredentials.items.map(item => (
authCredentials.removeSecret(item.key)}
/>
))}
{apiKeys.items.map(item => (
apiKeys.removeSecret(item.key)}
/>
))}
)
}
```
---
## Type Safety
All hooks are fully typed with TypeScript:
```tsx
import type {
AsyncState,
HookError,
VoidAsyncState
} from 'react-native-sensitive-info/hooks'
const { data, error, isLoading }: AsyncState = useSecretItem('token')
const hookError: HookError = error
const originalError: unknown = error?.originalError
```
---
## Troubleshooting
### Hooks return loading state but never complete
**Solution:** Check for errors in the console. Ensure proper options are passed.
### Memory warnings during testing
**Solution:** Hooks automatically clean up. Ensure you're waiting for async operations in tests:
```tsx
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
```
---
## Contributing
We welcome contributions! Please ensure:
- All memory cleanup is handled
- Hooks follow React Rules of Hooks
- TypeScript types are comprehensive
- Examples are provided for new hooks
---
## License
MIT © Mateus Andrade