@fluxbase/sdk-react
Version:
React hooks for Fluxbase SDK
1,077 lines (921 loc) • 28.7 kB
Markdown
# Fluxbase React Admin Hooks
Comprehensive React hooks for building admin dashboards and management interfaces with Fluxbase.
## Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Admin Authentication](#admin-authentication)
- [User Management](#user-management)
- [API Keys](#api-keys)
- [Webhooks](#webhooks)
- [Settings Management](#settings-management)
- [Complete Examples](#complete-examples)
## Installation
```bash
npm install @fluxbase/sdk @fluxbase/sdk-react @tanstack/react-query
```
## Quick Start
```tsx
import { createClient } from '@fluxbase/sdk'
import { FluxbaseProvider } from '@fluxbase/sdk-react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import AdminDashboard from './AdminDashboard'
const client = createClient({ url: 'http://localhost:8080' })
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<FluxbaseProvider client={client}>
<AdminDashboard />
</FluxbaseProvider>
</QueryClientProvider>
)
}
```
## Admin Authentication
### useAdminAuth
Hook for managing admin authentication state.
```tsx
import { useAdminAuth } from '@fluxbase/sdk-react'
function AdminLogin() {
const { user, isAuthenticated, isLoading, error, login, logout } = useAdminAuth({
autoCheck: true // Automatically check if admin is authenticated on mount
})
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
try {
await login(email, password)
// Redirect to admin dashboard
} catch (err) {
console.error('Login failed:', err)
}
}
if (isLoading) {
return <div>Checking authentication...</div>
}
if (isAuthenticated) {
return (
<div>
<p>Logged in as: {user?.email}</p>
<p>Role: {user?.role}</p>
<button onClick={logout}>Logout</button>
</div>
)
}
return (
<form onSubmit={handleLogin}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Admin email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit">Login</button>
{error && <p className="error">{error.message}</p>}
</form>
)
}
```
### Protected Admin Routes
```tsx
import { useAdminAuth } from '@fluxbase/sdk-react'
import { Navigate } from 'react-router-dom'
function ProtectedAdminRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAdminAuth({ autoCheck: true })
if (isLoading) {
return <div>Loading...</div>
}
if (!isAuthenticated) {
return <Navigate to="/admin/login" replace />
}
return <>{children}</>
}
// Usage
<Routes>
<Route path="/admin/login" element={<AdminLogin />} />
<Route
path="/admin/*"
element={
<ProtectedAdminRoute>
<AdminDashboard />
</ProtectedAdminRoute>
}
/>
</Routes>
```
## User Management
### useUsers
Hook for managing users with pagination and CRUD operations.
```tsx
import { useUsers } from '@fluxbase/sdk-react'
import { useState } from 'react'
function UserManagement() {
const [page, setPage] = useState(0)
const limit = 20
const {
users,
total,
isLoading,
error,
refetch,
inviteUser,
updateUserRole,
deleteUser,
resetPassword
} = useUsers({
autoFetch: true,
limit,
offset: page * limit
})
const handleInvite = async () => {
const email = prompt('Enter email:')
const role = confirm('Admin role?') ? 'admin' : 'user'
if (email) {
await inviteUser(email, role)
}
}
const handleRoleChange = async (userId: string, currentRole: string) => {
const newRole = currentRole === 'admin' ? 'user' : 'admin'
await updateUserRole(userId, newRole)
}
const handleDelete = async (userId: string) => {
if (confirm('Delete this user?')) {
await deleteUser(userId)
}
}
const handleResetPassword = async (userId: string) => {
const newPassword = await resetPassword(userId)
alert(`New password: ${newPassword}`)
}
if (isLoading) return <div>Loading users...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
<div className="header">
<h2>User Management ({total} users)</h2>
<button onClick={handleInvite}>Invite User</button>
<button onClick={refetch}>Refresh</button>
</div>
<table>
<thead>
<tr>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{user.email}</td>
<td>
<span className={`badge ${user.role}`}>{user.role}</span>
</td>
<td>
<span className={`status ${user.email_confirmed ? 'confirmed' : 'pending'}`}>
{user.email_confirmed ? 'Confirmed' : 'Pending'}
</span>
</td>
<td>{new Date(user.created_at).toLocaleDateString()}</td>
<td>
<button onClick={() => handleRoleChange(user.id, user.role)}>
Toggle Role
</button>
<button onClick={() => handleResetPassword(user.id)}>
Reset Password
</button>
<button onClick={() => handleDelete(user.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="pagination">
<button disabled={page === 0} onClick={() => setPage(page - 1)}>
Previous
</button>
<span>
Page {page + 1} of {Math.ceil(total / limit)}
</span>
<button
disabled={(page + 1) * limit >= total}
onClick={() => setPage(page + 1)}
>
Next
</button>
</div>
</div>
)
}
```
### User Search and Filters
```tsx
import { useUsers } from '@fluxbase/sdk-react'
import { useState, useEffect } from 'react'
function UserSearch() {
const [searchEmail, setSearchEmail] = useState('')
const [roleFilter, setRoleFilter] = useState<'admin' | 'user' | undefined>()
const { users, isLoading, refetch } = useUsers({
autoFetch: true,
email: searchEmail || undefined,
role: roleFilter
})
// Refetch when filters change
useEffect(() => {
refetch()
}, [searchEmail, roleFilter, refetch])
return (
<div>
<input
type="text"
placeholder="Search by email..."
value={searchEmail}
onChange={(e) => setSearchEmail(e.target.value)}
/>
<select value={roleFilter || ''} onChange={(e) => setRoleFilter(e.target.value as any)}>
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
{isLoading ? (
<div>Searching...</div>
) : (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.email} - {user.role}
</li>
))}
</ul>
)}
</div>
)
}
```
## API Keys
### useAPIKeys
Hook for managing API keys.
```tsx
import { useAPIKeys } from '@fluxbase/sdk-react'
import { useState } from 'react'
function APIKeyManagement() {
const { keys, isLoading, error, createKey, updateKey, revokeKey, deleteKey } = useAPIKeys({
autoFetch: true
})
const [showCreateForm, setShowCreateForm] = useState(false)
const [newKeyData, setNewKeyData] = useState<{
name: string
description: string
expiresInDays: number
}>({ name: '', description: '', expiresInDays: 365 })
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault()
try {
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + newKeyData.expiresInDays)
const result = await createKey({
name: newKeyData.name,
description: newKeyData.description,
expires_at: expiresAt.toISOString()
})
// Show the key (only time it's visible)
alert(`API Key created!\n\nKey: ${result.key}\n\nSave this securely - it won't be shown again!`)
setShowCreateForm(false)
setNewKeyData({ name: '', description: '', expiresInDays: 365 })
} catch (err) {
console.error('Failed to create key:', err)
}
}
const handleRevoke = async (keyId: string) => {
if (confirm('Revoke this API key? It will immediately stop working.')) {
await revokeKey(keyId)
}
}
const handleDelete = async (keyId: string) => {
if (confirm('Permanently delete this API key?')) {
await deleteKey(keyId)
}
}
if (isLoading) return <div>Loading API keys...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
<div className="header">
<h2>API Keys ({keys.length})</h2>
<button onClick={() => setShowCreateForm(!showCreateForm)}>
{showCreateForm ? 'Cancel' : 'Create New Key'}
</button>
</div>
{showCreateForm && (
<form onSubmit={handleCreate} className="create-form">
<h3>Create New API Key</h3>
<input
type="text"
placeholder="Key name (e.g., Backend Service)"
value={newKeyData.name}
onChange={(e) => setNewKeyData({ ...newKeyData, name: e.target.value })}
required
/>
<textarea
placeholder="Description (optional)"
value={newKeyData.description}
onChange={(e) => setNewKeyData({ ...newKeyData, description: e.target.value })}
/>
<label>
Expires in:
<input
type="number"
min="1"
max="3650"
value={newKeyData.expiresInDays}
onChange={(e) => setNewKeyData({ ...newKeyData, expiresInDays: parseInt(e.target.value) })}
/>
days
</label>
<button type="submit">Create Key</button>
</form>
)}
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Created</th>
<th>Expires</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{keys.map((key) => (
<tr key={key.id}>
<td>{key.name}</td>
<td>{key.description}</td>
<td>{new Date(key.created_at).toLocaleDateString()}</td>
<td>
{key.expires_at ? (
<span className={new Date(key.expires_at) < new Date() ? 'expired' : ''}>
{new Date(key.expires_at).toLocaleDateString()}
</span>
) : (
'Never'
)}
</td>
<td>
<button onClick={() => handleRevoke(key.id)}>Revoke</button>
<button onClick={() => handleDelete(key.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
```
## Webhooks
### useWebhooks
Hook for managing webhooks and monitoring deliveries.
```tsx
import { useWebhooks } from '@fluxbase/sdk-react'
import { useState } from 'react'
function WebhookManagement() {
const {
webhooks,
isLoading,
error,
createWebhook,
updateWebhook,
deleteWebhook,
testWebhook,
getDeliveries,
retryDelivery
} = useWebhooks({ autoFetch: true })
const [showCreateForm, setShowCreateForm] = useState(false)
const [selectedWebhook, setSelectedWebhook] = useState<string | null>(null)
const [deliveries, setDeliveries] = useState<any[]>([])
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
await createWebhook({
name: formData.get('name') as string,
url: formData.get('url') as string,
events: ['INSERT', 'UPDATE', 'DELETE'],
table: formData.get('table') as string,
schema: 'public',
enabled: true,
secret: `webhook_secret_${Math.random().toString(36).substring(7)}`
})
setShowCreateForm(false)
}
const handleTest = async (webhookId: string) => {
try {
await testWebhook(webhookId)
alert('Test webhook sent! Check your endpoint.')
} catch (err) {
alert('Test failed: ' + (err as Error).message)
}
}
const handleToggle = async (webhookId: string, currentlyEnabled: boolean) => {
await updateWebhook(webhookId, { enabled: !currentlyEnabled })
}
const viewDeliveries = async (webhookId: string) => {
const result = await getDeliveries(webhookId, { limit: 20 })
setDeliveries(result.deliveries)
setSelectedWebhook(webhookId)
}
if (isLoading) return <div>Loading webhooks...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
<div className="header">
<h2>Webhooks ({webhooks.length})</h2>
<button onClick={() => setShowCreateForm(!showCreateForm)}>
{showCreateForm ? 'Cancel' : 'Create Webhook'}
</button>
</div>
{showCreateForm && (
<form onSubmit={handleCreate} className="create-form">
<h3>Create New Webhook</h3>
<input name="name" placeholder="Webhook name" required />
<input name="url" type="url" placeholder="https://example.com/webhook" required />
<input name="table" placeholder="Table name (e.g., users)" required />
<button type="submit">Create</button>
</form>
)}
<table>
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th>Table</th>
<th>Events</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{webhooks.map((webhook) => (
<tr key={webhook.id}>
<td>{webhook.name}</td>
<td className="url">{webhook.url}</td>
<td>{webhook.schema}.{webhook.table}</td>
<td>{webhook.events.join(', ')}</td>
<td>
<span className={`status ${webhook.enabled ? 'enabled' : 'disabled'}`}>
{webhook.enabled ? 'Enabled' : 'Disabled'}
</span>
</td>
<td>
<button onClick={() => handleToggle(webhook.id, webhook.enabled)}>
{webhook.enabled ? 'Disable' : 'Enable'}
</button>
<button onClick={() => handleTest(webhook.id)}>Test</button>
<button onClick={() => viewDeliveries(webhook.id)}>Deliveries</button>
<button onClick={() => deleteWebhook(webhook.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
{selectedWebhook && (
<div className="deliveries">
<h3>Recent Deliveries</h3>
<button onClick={() => setSelectedWebhook(null)}>Close</button>
<table>
<thead>
<tr>
<th>Status</th>
<th>Response Code</th>
<th>Attempt</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{deliveries.map((delivery) => (
<tr key={delivery.id}>
<td>
<span className={`status ${delivery.status}`}>{delivery.status}</span>
</td>
<td>{delivery.response_status_code || 'N/A'}</td>
<td>{delivery.attempt_count}</td>
<td>{new Date(delivery.created_at).toLocaleString()}</td>
<td>
{delivery.status === 'failed' && (
<button onClick={() => retryDelivery(selectedWebhook, delivery.id)}>
Retry
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)
}
```
## Settings Management
### useAppSettings
Hook for managing application-wide settings.
```tsx
import { useAppSettings } from '@fluxbase/sdk-react'
function AppSettingsPanel() {
const { settings, isLoading, error, updateSettings } = useAppSettings({
autoFetch: true
})
const handleToggleFeature = async (feature: string, enabled: boolean) => {
await updateSettings({
features: {
...settings?.features,
[feature]: enabled
}
})
}
const handleUpdateSecurity = async (e: React.FormEvent) => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
await updateSettings({
security: {
enable_rate_limiting: formData.get('rateLimiting') === 'on',
rate_limit_requests_per_minute: parseInt(formData.get('rateLimit') as string)
}
})
}
if (isLoading) return <div>Loading settings...</div>
if (error) return <div>Error: {error.message}</div>
if (!settings) return <div>No settings found</div>
return (
<div>
<h2>Application Settings</h2>
<section>
<h3>Features</h3>
<label>
<input
type="checkbox"
checked={settings.features?.enable_realtime ?? false}
onChange={(e) => handleToggleFeature('enable_realtime', e.target.checked)}
/>
Enable Realtime
</label>
<label>
<input
type="checkbox"
checked={settings.features?.enable_storage ?? false}
onChange={(e) => handleToggleFeature('enable_storage', e.target.checked)}
/>
Enable Storage
</label>
<label>
<input
type="checkbox"
checked={settings.features?.enable_functions ?? false}
onChange={(e) => handleToggleFeature('enable_functions', e.target.checked)}
/>
Enable Functions
</label>
</section>
<section>
<h3>Security</h3>
<form onSubmit={handleUpdateSecurity}>
<label>
<input
type="checkbox"
name="rateLimiting"
defaultChecked={settings.security?.enable_rate_limiting ?? false}
/>
Enable Rate Limiting
</label>
<label>
Requests per minute:
<input
type="number"
name="rateLimit"
defaultValue={settings.security?.rate_limit_requests_per_minute ?? 60}
min="1"
/>
</label>
<button type="submit">Update Security</button>
</form>
</section>
</div>
)
}
```
### useSystemSettings
Hook for managing system-wide key-value settings.
```tsx
import { useSystemSettings } from '@fluxbase/sdk-react'
import { useState } from 'react'
function SystemSettingsPanel() {
const { settings, isLoading, error, getSetting, updateSetting, deleteSetting } = useSystemSettings({
autoFetch: true
})
const [editingKey, setEditingKey] = useState<string | null>(null)
const [editValue, setEditValue] = useState('')
const handleEdit = (key: string, currentValue: any) => {
setEditingKey(key)
setEditValue(JSON.stringify(currentValue, null, 2))
}
const handleSave = async () => {
if (!editingKey) return
try {
const parsedValue = JSON.parse(editValue)
await updateSetting(editingKey, { value: parsedValue })
setEditingKey(null)
} catch (err) {
alert('Invalid JSON')
}
}
const handleCreate = async () => {
const key = prompt('Setting key:')
const valueStr = prompt('Setting value (JSON):')
const description = prompt('Description:')
if (key && valueStr) {
try {
const value = JSON.parse(valueStr)
await updateSetting(key, { value, description: description || undefined })
} catch (err) {
alert('Invalid JSON')
}
}
}
const handleDelete = async (key: string) => {
if (confirm(`Delete setting "${key}"?`)) {
await deleteSetting(key)
}
}
if (isLoading) return <div>Loading settings...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
<div className="header">
<h2>System Settings ({settings.length})</h2>
<button onClick={handleCreate}>Create Setting</button>
</div>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{settings.map((setting) => (
<tr key={setting.key}>
<td><code>{setting.key}</code></td>
<td>
{editingKey === setting.key ? (
<textarea
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
rows={5}
/>
) : (
<pre>{JSON.stringify(setting.value, null, 2)}</pre>
)}
</td>
<td>{setting.description}</td>
<td>
{editingKey === setting.key ? (
<>
<button onClick={handleSave}>Save</button>
<button onClick={() => setEditingKey(null)}>Cancel</button>
</>
) : (
<>
<button onClick={() => handleEdit(setting.key, setting.value)}>Edit</button>
<button onClick={() => handleDelete(setting.key)}>Delete</button>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
```
## Complete Examples
### Full Admin Dashboard
```tsx
import { useAdminAuth, useUsers, useAPIKeys, useWebhooks, useAppSettings } from '@fluxbase/sdk-react'
import { useState } from 'react'
function AdminDashboard() {
const { user, isAuthenticated, logout } = useAdminAuth({ autoCheck: true })
const { users, total: totalUsers } = useUsers({ autoFetch: true, limit: 5 })
const { keys } = useAPIKeys({ autoFetch: true })
const { webhooks } = useWebhooks({ autoFetch: true })
const { settings } = useAppSettings({ autoFetch: true })
const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'keys' | 'webhooks' | 'settings'>('overview')
if (!isAuthenticated) {
return <div>Please log in to access admin dashboard</div>
}
return (
<div className="admin-dashboard">
<header>
<h1>Admin Dashboard</h1>
<div className="user-info">
<span>{user?.email}</span>
<button onClick={logout}>Logout</button>
</div>
</header>
<nav>
<button onClick={() => setActiveTab('overview')}>Overview</button>
<button onClick={() => setActiveTab('users')}>Users</button>
<button onClick={() => setActiveTab('keys')}>API Keys</button>
<button onClick={() => setActiveTab('webhooks')}>Webhooks</button>
<button onClick={() => setActiveTab('settings')}>Settings</button>
</nav>
<main>
{activeTab === 'overview' && (
<div className="overview">
<h2>Overview</h2>
<div className="stats">
<div className="stat-card">
<h3>Total Users</h3>
<p className="stat-value">{totalUsers}</p>
</div>
<div className="stat-card">
<h3>API Keys</h3>
<p className="stat-value">{keys.length}</p>
</div>
<div className="stat-card">
<h3>Webhooks</h3>
<p className="stat-value">{webhooks.length}</p>
</div>
<div className="stat-card">
<h3>Realtime</h3>
<p className="stat-value">
{settings?.features?.enable_realtime ? 'Enabled' : 'Disabled'}
</p>
</div>
</div>
<div className="recent-users">
<h3>Recent Users</h3>
<ul>
{users.slice(0, 5).map((u) => (
<li key={u.id}>
{u.email} - {u.role}
</li>
))}
</ul>
</div>
</div>
)}
{activeTab === 'users' && <UserManagement />}
{activeTab === 'keys' && <APIKeyManagement />}
{activeTab === 'webhooks' && <WebhookManagement />}
{activeTab === 'settings' && <AppSettingsPanel />}
</main>
</div>
)
}
```
### Multi-Tab Admin Interface
```tsx
import { Tabs, TabList, Tab, TabPanels, TabPanel } from '@reach/tabs'
import {
useUsers,
useAPIKeys,
useWebhooks,
useAppSettings,
useSystemSettings
} from '@fluxbase/sdk-react'
function AdminTabs() {
return (
<Tabs>
<TabList>
<Tab>Users</Tab>
<Tab>API Keys</Tab>
<Tab>Webhooks</Tab>
<Tab>App Settings</Tab>
<Tab>System Settings</Tab>
</TabList>
<TabPanels>
<TabPanel>
<UserManagement />
</TabPanel>
<TabPanel>
<APIKeyManagement />
</TabPanel>
<TabPanel>
<WebhookManagement />
</TabPanel>
<TabPanel>
<AppSettingsPanel />
</TabPanel>
<TabPanel>
<SystemSettingsPanel />
</TabPanel>
</TabPanels>
</Tabs>
)
}
```
### Real-time Updates
All hooks support automatic refetching with the `refetchInterval` option:
```tsx
function LiveUserList() {
const { users, total } = useUsers({
autoFetch: true,
refetchInterval: 5000 // Refetch every 5 seconds
})
return (
<div>
<h2>Live Users ({total})</h2>
<ul>
{users.map((user) => (
<li key={user.id}>{user.email}</li>
))}
</ul>
<small>Updates every 5 seconds</small>
</div>
)
}
```
## Best Practices
### Error Handling
```tsx
function RobustComponent() {
const { users, error, isLoading, refetch } = useUsers({ autoFetch: true })
if (isLoading) {
return <LoadingSpinner />
}
if (error) {
return (
<ErrorState
message={error.message}
onRetry={refetch}
/>
)
}
return <UserList users={users} />
}
```
### Optimistic Updates
All mutation functions automatically refetch data after successful operations:
```tsx
const { users, inviteUser } = useUsers({ autoFetch: true })
// This will automatically refetch the user list after inviting
await inviteUser('new@example.com', 'user')
// users state is now updated with the new user
```
### Manual Refetch
```tsx
const { users, refetch } = useUsers({ autoFetch: false })
// Manually fetch when needed
useEffect(() => {
refetch()
}, [someCondition])
```
### Performance Optimization
```tsx
// Don't auto-fetch on mount if data isn't immediately needed
const { users, refetch } = useUsers({ autoFetch: false })
// Fetch only when tab is active
useEffect(() => {
if (isTabActive) {
refetch()
}
}, [isTabActive, refetch])
```
## TypeScript Support
All hooks are fully typed with comprehensive TypeScript interfaces:
```tsx
import type { EnrichedUser, APIKey, Webhook } from '@fluxbase/sdk-react'
function TypedComponent() {
const { users }: { users: EnrichedUser[] } = useUsers({ autoFetch: true })
const { keys }: { keys: APIKey[] } = useAPIKeys({ autoFetch: true })
const { webhooks }: { webhooks: Webhook[] } = useWebhooks({ autoFetch: true })
// Full type safety
}
```
## API Reference
### Common Hook Options
All hooks support these common options:
- `autoFetch?: boolean` - Automatically fetch data on component mount (default: `true`)
- `refetchInterval?: number` - Automatically refetch data every N milliseconds (default: `0` - disabled)
### Common Hook Returns
All hooks return these common fields:
- `isLoading: boolean` - Whether data is currently being fetched
- `error: Error | null` - Any error that occurred during fetch/mutation
- `refetch: () => Promise<void>` - Manually trigger a data refetch
## License
MIT