Type-safe, performant GunDB hooks for React/Preact with comprehensive error handling, loading states, and debugging utilities.
https://gun-react-todoapp.vercel.app/
Repo available at: https://github.com/alterx/gun-react-todoapp
- Type-safe - Comprehensive TypeScript definitions
- Performant - Optimized with memoization and proper cleanup
- Reliable - Built-in error handling and loading states
- Developer-friendly - Debug utilities and development warnings
- Zero memory leaks - Proper listener cleanup and memory management
- React & Preact - Support for both frameworks
- Real-time - Live updates with efficient debouncing
- Authentication - Built-in auth provider with key management and storage
- Pagination - High-performance paginated collections with smart caching
npm install @altrx/gundb-react-hooks
# or
yarn add @altrx/gundb-react-hooks
# or
pnpm add @altrx/gundb-react-hooks
import React from 'react';
import Gun from 'gun';
import { useGun, useGunState, useGunCollectionState } from '@altrx/gundb-react-hooks';
function App() {
const gun = useGun(Gun, { peers: ['http://localhost:8765/gun'] });
return <UserProfile gun={gun} />;
}
function UserProfile({ gun }) {
const {
fields: profile,
put,
error,
isLoading
} = useGunState(gun.get('user').get('profile'));
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.err}</div>;
return (
<div>
<h1>{profile.name || 'Anonymous'}</h1>
<button onClick={() => put({ name: 'John Doe' })}>
Update Name
</button>
</div>
);
}
import { AuthProvider, useAuth } from '@altrx/gundb-react-hooks';
function App() {
return (
<AuthProvider
Gun={Gun}
sea={Gun.SEA}
storage={localStorage}
gunOpts={{ peers: ['http://localhost:8765/gun'] }}
>
<AuthenticatedApp />
</AuthProvider>
);
}
function AuthenticatedApp() {
const { user, login, logout, isLoggedIn, appKeys } = useAuth();
const { fields: profile, put } = useGunState(
user?.get('profile'),
{ appKeys }
);
if (!isLoggedIn) {
return (
<div>
<button onClick={() => login()}>Login</button>
</div>
);
}
return (
<div>
<h1>Welcome, {profile.name || 'User'}!</h1>
<input
value={profile.name || ''}
onChange={(e) => put({ name: e.target.value })}
placeholder="Enter your name"
/>
<button onClick={() => logout()}>Logout</button>
</div>
);
}
interface UserProfile {
name: string;
email: string;
avatar?: string;
}
interface TodoItem {
text: string;
completed: boolean;
createdAt: string;
}
function TypeSafeApp() {
const gun = useGun(Gun, { peers: ['http://localhost:8765/gun'] });
// Type-safe user profile
const {
fields: profile,
put: updateProfile,
error: profileError,
isLoading: profileLoading
} = useGunState<UserProfile>(gun.get('user').get('profile'));
// Type-safe todo collection with pagination
const {
currentPageItems: todos,
addToSet: addTodo,
removeFromSet: removeTodo,
updateInSet: updateTodo,
error: todosError,
totalItems: todoCount,
currentPage,
totalPages,
nextPage,
prevPage,
pageSize,
setPageSize
} = useGunCollectionStatePaginated<TodoItem>(gun.get('todos'), {
pageSize: 10,
sortBy: 'createdAt',
sortOrder: 'desc',
preloadPages: 1
});
const handleAddTodo = async () => {
try {
await addTodo({
text: 'New todo',
completed: false,
createdAt: new Date().toISOString()
});
} catch (error) {
console.error('Failed to add todo:', error);
}
};
if (profileError || todosError) {
return <ErrorComponent error={profileError || todosError} />;
}
return (
<div>
<h1>{profile.name}'s Todos ({todoCount})</h1>
{/* Pagination Controls */}
<div>
<span>Page {currentPage + 1} of {totalPages}</span>
<select value={pageSize} onChange={(e) => setPageSize(Number(e.target.value))}>
<option value={5}>5 per page</option>
<option value={10}>10 per page</option>
<option value={20}>20 per page</option>
</select>
</div>
{/* Todo Items */}
{todos.map(todo => (
<TodoItem
key={todo.nodeID}
todo={todo}
onToggle={() => updateTodo(todo.nodeID, { completed: !todo.completed })}
onRemove={() => removeTodo(todo.nodeID)}
/>
))}
{/* Pagination Navigation */}
<div>
<button onClick={prevPage} disabled={currentPage === 0}>
Previous
</button>
<button onClick={nextPage} disabled={currentPage >= totalPages - 1}>
Next
</button>
</div>
<button onClick={handleAddTodo}>Add Todo</button>
</div>
);
}
Initialize a Gun instance.
const gun = useGun(Gun, {
peers: ['http://localhost:8765/gun'],
localStorage: true,
});
Manage state for a Gun node with error handling and loading states.
const {
fields, // T: The current state
put, // (data: Partial<T>) => Promise<void>
remove, // (field: string) => Promise<void>
error, // GunError | null
isLoading, // boolean
isConnected, // boolean
} = useGunState<T>(ref, options);
Manage collections (Sets) with comprehensive CRUD operations.
const {
collection, // Map<string, NodeT<T>>
items, // NodeT<T>[] - Array for easy iteration
addToSet, // (data: T, nodeID?: string) => Promise<void>
updateInSet, // (nodeID: string, data: Partial<T>) => Promise<void>
removeFromSet, // (nodeID: string) => Promise<void>
error, // GunError | null
isLoading, // boolean
count, // number
} = useGunCollectionState<T>(ref, options);
High-performance paginated collections with smart caching and real-time updates.
const {
currentPageItems, // NodeT<T>[] - Current page items
currentPage, // number - Current page index (0-based)
totalPages, // number - Total number of pages
pageSize, // number - Items per page
setPageSize, // (size: number) => void
nextPage, // () => void
prevPage, // () => void
goToPage, // (page: number) => void
hasNextPage, // boolean
hasPrevPage, // boolean
isLoadingPage, // boolean
totalItems, // number - Total items in collection
// ... all useGunCollectionState methods
addToSet, // (data: T, nodeID?: string) => Promise<void>
updateInSet, // (nodeID: string, data: Partial<T>) => Promise<void>
removeFromSet, // (nodeID: string) => Promise<void>
error, // GunError | null
preloadedPages, // Set<number> - Cached pages
} = useGunCollectionStatePaginated<T>(ref, options);
Handle authentication with key pairs.
const [
namespacedGraph, // IGunUserReference
isLoggedIn, // boolean
authError, // GunError | null
] = useGunKeyAuth(gun, keyPair, true);
Generate or use existing key pairs.
const keyPair = useGunKeys(SEA, existingKeys);
Debug hook for development - logs all updates.
// Only active in development
useGunDebug(userRef, 'UserProfile', process.env.NODE_ENV === 'development');
Monitor connection status and health.
const { isConnected, lastSeen, error } = useGunConnection(gun);
Provide Gun instance through React context.
<GunProvider gun={Gun} options={{ peers: ['http://localhost:8765/gun'] }}>
<App />
</GunProvider>
Access Gun instance from context.
const gun = useGunContext();
Comprehensive authentication provider with key management.
<AuthProvider
Gun={Gun}
sea={Gun.SEA}
storage={localStorage}
gunOpts={{ peers: ['http://localhost:8765/gun'] }}
>
<App />
</AuthProvider>
Access authentication state and methods.
const {
user, // IGunUserReference
login, // (keys?: KeyPair) => void
logout, // (callback?: () => void) => void
isLoggedIn, // boolean
appKeys, // KeyPair | undefined
gun, // IGunChainReference
} = useAuth();
interface GunError {
err: string;
code?: string | number;
context?: string;
}
interface NodeData<T> {
nodeID: string;
// ... your data
}
interface UseGunStateReturn<T> {
fields: T;
put: (data: Partial<T>) => Promise<void>;
remove: (field: string) => Promise<void>;
error: GunError | null;
isLoading: boolean;
isConnected: boolean;
}
function MyComponent() {
const { fields, put, error } = useGunState(ref);
// Global error handling
if (error) {
return <ErrorBoundary error={error} />;
}
// Operation-specific error handling
const handleSave = async () => {
try {
await put(data);
showSuccessMessage();
} catch (error) {
showErrorMessage(error.err);
}
};
}
function DebuggedComponent() {
const gun = useGunContext();
const userRef = gun.user().get('profile');
// Debug all updates in development
useGunDebug(userRef, 'UserProfile');
const { fields } = useGunState(userRef);
return <div>{fields.name}</div>;
}
// Memoize expensive operations
const expensiveData = useMemo(() => {
return items
.filter((item) => item.category === selectedCategory)
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}, [items, selectedCategory]);
// Use callback for event handlers
const handleAddItem = useCallback(
async (item) => {
await addToSet(item);
},
[addToSet],
);
v1.0.0 introduces useGunCollectionStatePaginated
for high-performance collection management:
// Before: Basic collection
const { items, addToSet } = useGunCollectionState(ref);
// After: Paginated with optimization
const { currentPageItems, addToSet, nextPage, totalPages } =
useGunCollectionStatePaginated(ref, {
pageSize: 20,
sortBy: 'createdAt',
preloadPages: 2,
});
- Memory Efficient: Only loads current page + preloaded pages
- Smart Caching: 5-minute page cache with automatic invalidation
- Real-time Updates: Debounced live synchronization
- Type Safety: Full TypeScript support with inference
For detailed migration instructions and compatibility information, see the detailed documentation for each hook.
// Enable debug logging for specific components
useGunDebug(ref, 'ComponentName', true);
// Connection monitoring
const { isConnected, lastSeen } = useGunConnection(gun);
function AppWithErrorBoundary() {
return (
<ErrorBoundary>
<GunProvider gun={Gun} options={opts}>
<App />
</GunProvider>
</ErrorBoundary>
);
}
- useGun
- useGunNamespace
- useGunKeyAuth
- useGunKeys
- useGunOnNodeUpdated
- useGunState
- useGunCollectionState
- useGunCollectionStatePaginated
- useAuth
- AuthProvider
We welcome contributions! Please see our contributing guidelines and:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Add tests for your changes
- Ensure type safety with TypeScript
- Submit a pull request
Licensed under MIT.