--- title: React Hooks sidebarTitle: Hooks --- Spacedrive provides type-safe React hooks for data fetching, mutations, and event subscriptions. All types are auto-generated from Rust definitions. ## Data Fetching Hooks ### useCoreQuery Type-safe hook for core-scoped queries (operations that don't require a library). ```tsx import { useCoreQuery } from '@sd/interface'; function LibraryList() { const { data: libraries, isLoading, error } = useCoreQuery({ type: 'libraries.list', input: { include_stats: false }, }); if (isLoading) return ; if (error) return
Error: {error.message}
; return (
{libraries?.map(lib => (
{lib.name}
))}
); } ``` **Features:** - **Auto-generated types** - `type` and `input` are type-checked against Rust definitions - **TanStack Query** - Full TanStack Query API available - **Automatic caching** - Query results are cached by key - **Type inference** - `data` type is automatically inferred **Examples:** ```tsx // Get node information const { data: nodeInfo } = useCoreQuery({ type: 'node.info', input: {}, }); // List all libraries with stats const { data: libraries } = useCoreQuery({ type: 'libraries.list', input: { include_stats: true }, }); // With TanStack Query options const { data: libraries } = useCoreQuery( { type: 'libraries.list', input: {}, }, { staleTime: 5 * 60 * 1000, // 5 minutes refetchOnWindowFocus: false, } ); ``` ### useLibraryQuery Type-safe hook for library-scoped queries (operations within a specific library). ```tsx import { useLibraryQuery } from '@sd/interface'; function FileExplorer({ path }: { path: string }) { const { data, isLoading } = useLibraryQuery({ type: 'files.directory_listing', input: { path }, }); if (isLoading) return ; return (
{data?.entries.map(entry => (
{entry.name}
))}
); } ``` **Features:** - **Automatic library scoping** - Uses current library ID from client - **Library switching** - Automatically refetches when library changes - **Type safety** - Input/output types inferred from operation - **Disabled when no library** - Query is disabled if no library selected **Examples:** ```tsx // Get directory listing const { data: files } = useLibraryQuery({ type: 'files.directory_listing', input: { path: '/photos' }, }); // List all locations const { data: locations } = useLibraryQuery({ type: 'locations.list', input: {}, }); // Get file metadata const { data: metadata } = useLibraryQuery({ type: 'files.get_metadata', input: { path: '/photos/IMG_1234.jpg' }, }); // Search files const { data: results } = useLibraryQuery({ type: 'files.search', input: { query: 'vacation', filters: [] }, }); ``` ### useNormalizedCache Event-driven cache updates for real-time sync across devices. See [Normalized Cache](/react/ui/normalized-cache) for full documentation. ```tsx import { useNormalizedCache } from '@sd/interface'; function LocationsList() { const { data: locations } = useNormalizedCache({ wireMethod: 'query:locations.list', input: {}, resourceType: 'location', isGlobalList: true, }); return (
{locations?.map(location => (
{location.name}
))}
); } ``` **When to use:** - List queries that need instant updates across devices - File listings that change frequently - Real-time resource monitoring ## Mutation Hooks ### useCoreMutation Type-safe hook for core-scoped mutations (operations that don't require a library). ```tsx import { useCoreMutation } from '@sd/interface'; function CreateLibraryButton() { const createLib = useCoreMutation('libraries.create'); const handleCreate = () => { createLib.mutate( { name: 'My Library', path: null }, { onSuccess: (library) => { console.log('Created:', library.name); }, onError: (error) => { console.error('Failed:', error.message); }, } ); }; return ( ); } ``` **Features:** - **Type-safe input** - Mutation input is type-checked - **Type-safe output** - Success callback receives typed result - **Loading states** - `isPending`, `isSuccess`, `isError` - **TanStack Query integration** - Full mutation API available **Examples:** ```tsx // Create library const createLib = useCoreMutation('libraries.create'); createLib.mutate({ name: 'Photos', path: '/Users/me/Photos' }); // Delete library const deleteLib = useCoreMutation('libraries.delete'); deleteLib.mutate({ id: '123' }); // Update node config const updateConfig = useCoreMutation('node.update_config'); updateConfig.mutate({ theme: 'dark', port: 8080 }); ``` ### useLibraryMutation Type-safe hook for library-scoped mutations (operations within a specific library). ```tsx import { useLibraryMutation } from '@sd/interface'; function ApplyTagsButton({ fileIds, tagIds }: { fileIds: number[], tagIds: string[] }) { const applyTags = useLibraryMutation('tags.apply'); return ( ); } ``` **Features:** - **Automatic library scoping** - Uses current library ID - **Type safety** - Input/output types inferred - **Error handling** - Throws if no library selected - **TanStack Query callbacks** - onSuccess, onError, onSettled **Examples:** ```tsx // Create location const createLocation = useLibraryMutation('locations.create'); createLocation.mutate({ path: '/Users/me/Photos', index_mode: 'deep' }); // Delete files const deleteFiles = useLibraryMutation('files.delete'); deleteFiles.mutate({ paths: ['/photo1.jpg', '/photo2.jpg'] }); // Apply tags const applyTags = useLibraryMutation('tags.apply'); applyTags.mutate({ entry_ids: [1, 2, 3], tag_ids: ['tag-uuid'] }); // Move files const moveFiles = useLibraryMutation('files.move'); moveFiles.mutate({ source: '/old/path', destination: '/new/path' }); ``` ## Event Hooks ### useEvent Subscribe to specific Spacedrive events. ```tsx import { useEvent } from '@sd/interface'; function JobProgress() { const [progress, setProgress] = useState(0); useEvent('JobProgress', (event) => { const jobProgress = event.JobProgress; setProgress(jobProgress.progress_percentage); }); return ; } ``` **Features:** - **Event filtering** - Only receives events of specified type - **Automatic cleanup** - Unsubscribes on unmount - **Type-safe** - Event type is checked at compile time **Examples:** ```tsx // Listen for file creation useEvent('FileCreated', (event) => { console.log('New file:', event.FileCreated.path); }); // Listen for indexing progress useEvent('IndexingProgress', (event) => { const { location_id, progress } = event.IndexingProgress; updateProgress(location_id, progress); }); // Listen for library sync events useEvent('LibrarySynced', (event) => { console.log('Library synced:', event.LibrarySynced.library_id); }); // Listen for job completion useEvent('JobCompleted', (event) => { const job = event.JobCompleted; toast.success(`Job ${job.name} completed!`); }); ``` ### useAllEvents Subscribe to all Spacedrive events (useful for debugging). ```tsx import { useAllEvents } from '@sd/interface'; function EventDebugger() { useAllEvents((event) => { console.log('Event:', event); }); return
Check console for events
; } ``` **Warning:** This can be noisy. Use `useEvent` for specific events in production. ## Custom Hooks ### useLibraries Convenience hook for fetching all libraries. ```tsx import { useLibraries } from '@sd/interface'; function LibraryDropdown() { const { data: libraries, isLoading } = useLibraries(); if (isLoading) return ; return ( ); } ``` **With stats:** ```tsx const { data: libraries } = useLibraries(true); libraries?.forEach(lib => { console.log(`${lib.name}: ${lib.statistics?.total_files} files`); }); ``` ## Client Hook ### useSpacedriveClient Access the Spacedrive client instance directly. ```tsx import { useSpacedriveClient } from '@sd/interface'; function LibrarySwitcher() { const client = useSpacedriveClient(); const [currentLibraryId, setCurrentLibraryId] = useState(null); const switchLibrary = (id: string) => { client.setCurrentLibrary(id); setCurrentLibraryId(id); }; return (
); } ``` **Client methods:** - `client.setCurrentLibrary(id: string)` - Switch to a library - `client.getCurrentLibraryId()` - Get current library ID - `client.execute(method, input)` - Execute RPC method directly - `client.on(event, handler)` - Subscribe to events - `client.off(event, handler)` - Unsubscribe from events ## Hook Patterns ### Combining Queries ```tsx function Dashboard() { const { data: libraries } = useLibraries(); const { data: nodeInfo } = useCoreQuery({ type: 'node.info', input: {}, }); const { data: jobs } = useLibraryQuery({ type: 'jobs.list', input: {}, }); return (

{nodeInfo?.name}

{libraries?.length} libraries

{jobs?.length} running jobs

); } ``` ### Dependent Queries ```tsx function FileDetails({ path }: { path: string }) { // First get file info const { data: file } = useLibraryQuery({ type: 'files.get', input: { path }, }); // Then get metadata (only if file exists) const { data: metadata } = useLibraryQuery( { type: 'files.get_metadata', input: { file_id: file?.id }, }, { enabled: !!file, } ); return
{metadata?.size} bytes
; } ``` ### Mutations with Refetch ```tsx function DeleteButton({ fileId }: { fileId: number }) { const queryClient = useQueryClient(); const deleteFile = useLibraryMutation('files.delete'); const handleDelete = () => { deleteFile.mutate( { file_ids: [fileId] }, { onSuccess: () => { // Refetch file list after deletion queryClient.invalidateQueries(['files.directory_listing']); }, } ); }; return ; } ``` ### Optimistic Updates ```tsx function RenameButton({ fileId, currentName }: { fileId: number, currentName: string }) { const queryClient = useQueryClient(); const rename = useLibraryMutation('files.rename'); const handleRename = (newName: string) => { rename.mutate( { file_id: fileId, new_name: newName }, { // Optimistically update UI before server confirms onMutate: async (variables) => { // Cancel outgoing refetches await queryClient.cancelQueries(['files.get', fileId]); // Snapshot previous value const previous = queryClient.getQueryData(['files.get', fileId]); // Optimistically update queryClient.setQueryData(['files.get', fileId], (old: any) => ({ ...old, name: variables.new_name, })); return { previous }; }, // Rollback on error onError: (err, variables, context) => { queryClient.setQueryData(['files.get', fileId], context?.previous); }, // Refetch after success or error onSettled: () => { queryClient.invalidateQueries(['files.get', fileId]); }, } ); }; return ; } ``` ## Best Practices ### Use the Right Hook ```tsx // Good - core query for libraries const { data: libraries } = useCoreQuery({ type: 'libraries.list', input: {}, }); // Bad - library query for core operation const { data: libraries } = useLibraryQuery({ type: 'libraries.list', // Type error! input: {}, }); // Good - library query for files const { data: files } = useLibraryQuery({ type: 'files.directory_listing', input: { path: '/' }, }); ``` ### Set Appropriate Stale Times ```tsx // Static data - long stale time const { data: nodeInfo } = useCoreQuery( { type: 'node.info', input: {}, }, { staleTime: 5 * 60 * 1000, // 5 minutes } ); // Dynamic data - short stale time or use useNormalizedCache const { data: files } = useNormalizedCache({ wireMethod: 'query:files.directory_listing', input: { path }, resourceType: 'file', }); ``` ### Handle Loading and Error States ```tsx // Good - handles all states function FileList() { const { data, isLoading, error } = useLibraryQuery({ type: 'files.directory_listing', input: { path: '/' }, }); if (isLoading) return ; if (error) return ; if (!data) return ; return
{data.entries.map(...)}
; } // Bad - assumes data exists function FileList() { const { data } = useLibraryQuery({ type: 'files.directory_listing', input: { path: '/' }, }); return
{data.entries.map(...)}
; // Can crash! } ``` ### Invalidate Related Queries ```tsx const createLocation = useLibraryMutation('locations.create'); const handleCreate = () => { createLocation.mutate( { path: '/photos' }, { onSuccess: () => { // Invalidate related queries queryClient.invalidateQueries(['locations.list']); queryClient.invalidateQueries(['files.directory_listing']); }, } ); }; ``` ## Summary Spacedrive's React hooks provide: - **Type safety** - All operations are type-checked against Rust definitions - **Auto-generation** - Types are generated from Rust, never manually written - **TanStack Query** - Full TanStack Query API available - **Library scoping** - Automatic library ID management - **Event subscriptions** - Real-time updates via WebSocket - **Normalized cache** - Instant cross-device sync Use `useCoreQuery` and `useLibraryQuery` for data fetching, `useCoreMutation` and `useLibraryMutation` for mutations, and `useEvent` for event subscriptions.