GoatDB React Hooks
The hooks are built on top of GoatDB’s core functionality, providing a more ergonomic interface for React components. They handle all the complexity of data synchronization and updates, making it easy to build reactive UIs that work seamlessly both online and offline.
Hooks Overview
useDB()
Initializes and returns the default GoatDB instance, handling storage and server synchronization automatically. The hook also bootstraps the database by setting up the storage backend and creating an initial connection to the server. This ensures that the application is ready to interact with the database without requiring additional setup steps.
- Behavior:
- Uses the Origin Private File System (OPFS) for storage
- Synchronizes with the server in the background
- Triggers re-renders when the current user changes
- Returns: A
GoatDB
instance
Example:
const db = useDB();
The
useDB
hook maintains a single instance of the database throughout your application’s lifecycle. All subsequent calls touseDB
will return the same instance, ensuring consistent state management across your components.
useDBReady()
Monitors the database’s loading state. Use it to manage your application’s initial loading screen. During this phase, the client loads locally stored data, establishes a server connection, and initializes an anonymous session if required.
- Returns:
"loading"
: Database is initializing"ready"
: Database is fully loaded and synchronized"error"
: An error occurred during initialization
Example:
function App() {
const dbStatus = useDBReady();
if (dbStatus === 'loading') {
return <LoadingScreen />;
} else if (dbStatus === 'error') {
return <ErrorScreen message='Failed to load database.' />;
}
return <MainApp />;
}
During the initial session setup, the client may require a network connection in order to download the initial copy of the history. Once this setup is complete, full offline functionality is supported.
useQuery()
Creates a new query or retrieves an existing one. On first access, GoatDB automatically loads the source repository either from the local disk or by fetching it from the server. The hook triggers UI re-rendering whenever the query results are updated, regardless of whether the changes originate from local or remote edits.
When a query is first opened, it performs a linear scan of its source using a coroutine without blocking the main thread. During and after this initial scan, the query caches its results to disk, allowing subsequent runs to resume execution from the cached state.
Config Options:
schema
(required): Specifies the schema(s) for the query resultssource
(required): Path to a repository or another query instancepredicate
(optional): Function to filter resultssortDescriptor
(optional): Function to sort resultsctx
(optional): Optional context for predicates and sort descriptorslimit
(optional): Limits the number of resultsshowIntermittentResults
(optional): Iftrue
, updates UI during initial scan
GoatDB re-evaluates the entire query whenever any of its configuration values change, including:
- The predicate function
- The sort descriptor function
- The context object
- The source repository
- The schema
GoatDB internally calls
.toString()
on the passed functions to determine if they have changed. While you don’t need to explicitly useuseCallback
or other memoization techniques, it’s crucial that your predicate and sort functions are pure functions:
- They should not modify any external state
- They should not depend on values that can change between calls
- They should not modify the items they receive
- Use the
ctx
parameter to pass in any external values needed
Example:
function TaskList() {
const tasksQuery = useQuery({
schema: taskSchema,
source: '/data/tasks',
sortDescriptor: (a, b) => a.get('text').localeCompare(b.get('text')),
predicate: (item) => !item.get('done'),
showIntermittentResults: true,
});
return (
<ul className='task-list'>
{tasksQuery.results().map((task) => (
<li key={task.path}>{task.get('text')}</li>
))}
</ul>
);
}
useItem()
Monitors changes to a specific item, triggering a re-render whenever the item’s state changes. It returns a mutable ManagedItem
instance that allows direct modifications. Any changes to the item are automatically queued for background commits and synchronized with the server.
Signatures:
useItem<S extends Schema>(...pathComps: string[]): ManagedItem<S> | undefined
useItem<S extends Schema>(path: string | undefined, opts: UseItemOpts): ManagedItem<S> | undefined
useItem<S extends Schema>(item: ManagedItem<S> | undefined, opts: UseItemOpts): ManagedItem<S> | undefined
- Options:
keys
(optional): Array of field names to track. Optimizes rendering by ignoring changes to other fields
Example:
function TaskEditor({ path }) {
const task = useItem(path, { keys: ['text'] });
if (!task) {
return <div>Loading task...</div>;
}
return (
<div className='task-editor'>
<label htmlFor='task-text'>Task:</label>
<input
id='task-text'
type='text'
value={task.get('text')}
onChange={(e) => task.set('text', e.target.value)}
/>
</div>
);
}
The
useItem
hook will automatically trigger a re-render when:
- The item becomes available after loading
- Any tracked field changes
- The schema changes
- The item is deleted or restored
Best Practices
Performance Optimization
-
Use
keys
withuseItem
: When you only need to track specific fields, use thekeys
option to prevent unnecessary re-renders. -
Memoize Predicates: Define predicate and sort functions outside your component or use
useCallback
to maintain stable references:const userPredicate = useCallback( ({ item }) => item.get('active') && item.get('role') === roleFilter, [roleFilter], );
-
Chain Queries: For complex data transformations, chain queries together rather than performing multiple operations in a single query.
Error Handling
-
Check for Undefined Items: Always handle the case where
useItem
returns undefined, which can happen during initial loading or if the item doesn’t exist. -
Monitor DB Ready State: Use
useDBReady
to handle loading and error states gracefully.
Data Synchronization
-
Background Writes: All writes are processed asynchronously. The system batches changes and writes them to both local storage and remote servers in parallel.
-
Offline Support: GoatDB maintains a local copy of the database and synchronizes changes when the connection is restored.
Technical Details
The React hooks are implemented using React’s useSyncExternalStore
to manage subscriptions to database changes. This ensures efficient updates and proper cleanup when components unmount.
-
Change Detection: The hooks use GoatDB’s mutation system to track changes at the field level, enabling precise updates.
-
Memory Management: Resources are automatically cleaned up when components unmount, preventing memory leaks.
-
Concurrency: The hooks handle concurrent updates gracefully, ensuring consistent state even when multiple components modify the same data.