@cranberry-money/shared-hooks
v1.0.41
Published
Shared business logic hooks for Blueberry platform - reusable across React and React Native
Readme
@cranberry-money/shared-hooks
Shared business logic hooks for the Blueberry platform. These hooks contain all the state management, validation, and business logic that can be reused across React (Blueberry) and React Native (Blackberry) applications.
Philosophy
This package follows the principle of "shared logic, separate UI":
- ✅ Business logic, validation, and state management live here
- ❌ UI components stay in their respective projects
- ✅ Each platform maintains its own look and feel
- ✅ Maximum code reuse without compromising platform-specific optimizations
Installation
npm install @cranberry-money/shared-hooksDependencies
This package has minimal dependencies:
@cranberry-money/shared-types- For TypeScript type definitions@cranberry-money/shared-constants- For business constants (optional)@cranberry-money/shared-utils- For utility functions (optional)
Note: This package does NOT depend on @cranberry-money/shared-services. Service functions are passed to hooks via dependency injection, keeping the packages decoupled.
Usage Examples
In Blueberry (React Web)
// src/pages/signin/SigninPage.tsx
import { useSignin } from '@cranberry-money/shared-hooks';
import { useNavigate } from 'react-router-dom';
import { signin } from '@cranberry-money/shared-services';
export const SigninPage = () => {
const navigate = useNavigate();
// Use the shared hook
const { form, errors, generalError, isLoading, showPassword, setFieldValue, togglePassword, handleSubmit } =
useSignin({
onSuccess: () => navigate('/dashboard'),
});
// Handle form submission
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
handleSubmit(signin);
};
// Render your UI using the hook's state and methods
return (
<div className="signin-container">
<form onSubmit={onSubmit}>
{generalError && <div className="error-alert">{generalError}</div>}
<input
type="email"
value={form.email}
onChange={(e) => setFieldValue('email', e.target.value)}
className={errors.email ? 'input-error' : ''}
/>
{errors.email && <span>{errors.email[0]}</span>}
<input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setFieldValue('password', e.target.value)}
/>
<button type="button" onClick={togglePassword}>
{showPassword ? 'Hide' : 'Show'}
</button>
{errors.password && <span>{errors.password[0]}</span>}
<button type="submit" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
</form>
</div>
);
};In Blackberry (React Native)
// src/screens/SignInScreen.tsx
import React from 'react';
import { View, Text, TextInput, TouchableOpacity, Alert } from 'react-native';
import { useSignin } from '@cranberry-money/shared-hooks';
import { signin } from '../services/authService';
export const SignInScreen = ({ navigation }) => {
// Use the same shared hook
const { form, errors, generalError, isLoading, showPassword, setFieldValue, togglePassword, handleSubmit } =
useSignin({
onSuccess: () => navigation.navigate('Dashboard'),
});
// Show alert for general errors
React.useEffect(() => {
if (generalError) {
Alert.alert('Sign In Failed', generalError);
}
}, [generalError]);
// Render your native UI using the hook's state and methods
return (
<View style={styles.container}>
<TextInput
style={[styles.input, errors.email && styles.inputError]}
value={form.email}
onChangeText={(text) => setFieldValue('email', text)}
placeholder="Email"
keyboardType="email-address"
autoCapitalize="none"
/>
{errors.email && <Text style={styles.error}>{errors.email[0]}</Text>}
<View style={styles.passwordContainer}>
<TextInput
style={[styles.input, errors.password && styles.inputError]}
value={form.password}
onChangeText={(text) => setFieldValue('password', text)}
placeholder="Password"
secureTextEntry={!showPassword}
/>
<TouchableOpacity onPress={togglePassword}>
<Text>{showPassword ? '👁' : '👁🗨'}</Text>
</TouchableOpacity>
</View>
{errors.password && <Text style={styles.error}>{errors.password[0]}</Text>}
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={() => handleSubmit(signin)}
disabled={isLoading}
>
<Text style={styles.buttonText}>{isLoading ? 'Signing in...' : 'Sign In'}</Text>
</TouchableOpacity>
</View>
);
};Available Hooks
useSignin
Manages signin form state, validation, and submission.
Options
interface UseSigninOptions {
onSuccess?: () => void; // Called after successful signin
onError?: (error: unknown) => void; // Called on signin error
}Returns
interface UseSigninReturn {
form: SigninPayload; // Form data
errors: FormErrors; // Field-specific errors
generalError: string | null; // General error message
isLoading: boolean; // Loading state
showPassword: boolean; // Password visibility state
setForm: (form: SigninPayload) => void; // Set entire form
setFieldValue: (field, value) => void; // Set individual field
clearErrors: () => void; // Clear all errors
togglePassword: () => void; // Toggle password visibility
validateForm: () => boolean; // Validate form manually
handleSubmit: (signinFn) => Promise; // Submit form with signin function
}Benefits
- Write once, use everywhere: Business logic is written once and shared
- Platform-appropriate UI: Each platform maintains its native look and feel
- Consistent behavior: Same validation, error handling, and state management
- Easy to test: Pure business logic separated from UI
- Type-safe: Full TypeScript support
Adding New Hooks
When creating new shared hooks:
- Create the hook in
src/hooks/ - Export it from
src/hooks/index.ts - Follow the pattern of returning both state and methods
- Keep UI concerns out - only business logic
- Make the hook flexible with options/callbacks
- Accept service functions as parameters (dependency injection) rather than importing them
Example structure:
interface UseYourFeatureOptions {
onSuccess?: () => void;
onError?: (error: unknown) => void;
}
export const useYourFeature = (options: UseYourFeatureOptions = {}) => {
// State management
const [state, setState] = useState();
// Business logic methods
const businessMethod = () => {
// Logic here
};
// Method that accepts service function via dependency injection
const handleAction = async (serviceFn: (data: any) => Promise<any>) => {
try {
const result = await serviceFn(state);
options.onSuccess?.();
} catch (error) {
options.onError?.(error);
}
};
// Return everything the UI needs
return {
// State
state,
// Methods
businessMethod,
handleAction, // Consumer passes their service function here
};
};Migration Guide
From Blueberry's Current Implementation
Before (in Blueberry):
// pages/signin/useSigninPage.ts
export const useSigninPage = () => {
// All logic here
};
// pages/signin/SigninPage.tsx
const { ... } = useSigninPage();After:
// pages/signin/SigninPage.tsx
import { useSignin } from '@cranberry-money/shared-hooks';
const { ... } = useSignin({
onSuccess: () => navigate('/dashboard'),
});From Blackberry's Current Implementation
Before (in Blackberry):
// screens/SignInScreen.tsx
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// Validation logic inlineAfter:
// screens/SignInScreen.tsx
import { useSignin } from '@cranberry-money/shared-hooks';
const { form, setFieldValue, ... } = useSignin({
onSuccess: () => navigation.navigate('Dashboard'),
});Testing
Since hooks are pure business logic, they're easy to test:
import { renderHook, act } from '@testing-library/react-hooks';
import { useSignin } from '@cranberry-money/shared-hooks';
test('validates email field', () => {
const { result } = renderHook(() => useSignin());
act(() => {
result.current.setFieldValue('email', 'invalid');
result.current.validateForm();
});
expect(result.current.errors.email).toBeDefined();
});Contributing
When adding new hooks, ensure they:
- Contain only business logic (no UI)
- Are well-typed with TypeScript
- Include comprehensive JSDoc comments
- Have corresponding tests
- Work identically on both platforms