Skip to main content
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).
import { useCoreQuery } from '@sd/interface';

function LibraryList() {
  const { data: libraries, isLoading, error } = useCoreQuery({
    type: 'libraries.list',
    input: { include_stats: false },
  });

  if (isLoading) return <Loader />;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {libraries?.map(lib => (
        <div key={lib.id}>{lib.name}</div>
      ))}
    </div>
  );
}
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:
// 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).
import { useLibraryQuery } from '@sd/interface';

function FileExplorer({ path }: { path: string }) {
  const { data, isLoading } = useLibraryQuery({
    type: 'files.directory_listing',
    input: { path },
  });

  if (isLoading) return <Loader />;

  return (
    <div>
      {data?.entries.map(entry => (
        <div key={entry.id}>{entry.name}</div>
      ))}
    </div>
  );
}
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:
// 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 for full documentation.
import { useNormalizedCache } from '@sd/interface';

function LocationsList() {
  const { data: locations } = useNormalizedCache({
    wireMethod: 'query:locations.list',
    input: {},
    resourceType: 'location',
    isGlobalList: true,
  });

  return (
    <div>
      {locations?.map(location => (
        <div key={location.id}>{location.name}</div>
      ))}
    </div>
  );
}
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).
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 (
    <button onClick={handleCreate} disabled={createLib.isPending}>
      {createLib.isPending ? 'Creating...' : 'Create Library'}
    </button>
  );
}
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:
// 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).
import { useLibraryMutation } from '@sd/interface';

function ApplyTagsButton({ fileIds, tagIds }: { fileIds: number[], tagIds: string[] }) {
  const applyTags = useLibraryMutation('tags.apply');

  return (
    <button
      onClick={() =>
        applyTags.mutate(
          { entry_ids: fileIds, tag_ids: tagIds },
          {
            onSuccess: () => {
              toast.success('Tags applied!');
            },
          }
        )
      }
      disabled={applyTags.isPending}
    >
      Apply Tags
    </button>
  );
}
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:
// 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.
import { useEvent } from '@sd/interface';

function JobProgress() {
  const [progress, setProgress] = useState(0);

  useEvent('JobProgress', (event) => {
    const jobProgress = event.JobProgress;
    setProgress(jobProgress.progress_percentage);
  });

  return <ProgressBar value={progress} />;
}
Features:
  • Event filtering - Only receives events of specified type
  • Automatic cleanup - Unsubscribes on unmount
  • Type-safe - Event type is checked at compile time
Examples:
// 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).
import { useAllEvents } from '@sd/interface';

function EventDebugger() {
  useAllEvents((event) => {
    console.log('Event:', event);
  });

  return <div>Check console for events</div>;
}
Warning: This can be noisy. Use useEvent for specific events in production.

Custom Hooks

useLibraries

Convenience hook for fetching all libraries.
import { useLibraries } from '@sd/interface';

function LibraryDropdown() {
  const { data: libraries, isLoading } = useLibraries();

  if (isLoading) return <Loader />;

  return (
    <select>
      {libraries?.map(lib => (
        <option key={lib.id} value={lib.id}>
          {lib.name}
        </option>
      ))}
    </select>
  );
}
With stats:
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.
import { useSpacedriveClient } from '@sd/interface';

function LibrarySwitcher() {
  const client = useSpacedriveClient();
  const [currentLibraryId, setCurrentLibraryId] = useState<string | null>(null);

  const switchLibrary = (id: string) => {
    client.setCurrentLibrary(id);
    setCurrentLibraryId(id);
  };

  return (
    <div>
      <button onClick={() => switchLibrary('lib-123')}>
        Switch to Library 123
      </button>
    </div>
  );
}
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

function Dashboard() {
  const { data: libraries } = useLibraries();
  const { data: nodeInfo } = useCoreQuery({
    type: 'node.info',
    input: {},
  });
  const { data: jobs } = useLibraryQuery({
    type: 'jobs.list',
    input: {},
  });

  return (
    <div>
      <h1>{nodeInfo?.name}</h1>
      <p>{libraries?.length} libraries</p>
      <p>{jobs?.length} running jobs</p>
    </div>
  );
}

Dependent Queries

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 <div>{metadata?.size} bytes</div>;
}

Mutations with Refetch

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 <button onClick={handleDelete}>Delete</button>;
}

Optimistic Updates

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 <button onClick={() => handleRename('New Name')}>Rename</button>;
}

Best Practices

Use the Right Hook

// 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

// 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

// Good - handles all states
function FileList() {
  const { data, isLoading, error } = useLibraryQuery({
    type: 'files.directory_listing',
    input: { path: '/' },
  });

  if (isLoading) return <Loader />;
  if (error) return <ErrorMessage error={error} />;
  if (!data) return <EmptyState />;

  return <div>{data.entries.map(...)}</div>;
}

// Bad - assumes data exists
function FileList() {
  const { data } = useLibraryQuery({
    type: 'files.directory_listing',
    input: { path: '/' },
  });

  return <div>{data.entries.map(...)}</div>; // Can crash!
}
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.