Skip to content

Hooks & Data Fetching

Craft Easy Admin provides four React hooks for interacting with the API. These hooks handle caching, pagination, ETag concurrency, and error states.

useAdminSchema

Fetches and caches the admin schema from /admin/schema.

function useAdminSchema(): {
  schema: AdminSchema | null;
  isLoading: boolean;
  error: string | null;
  getResource: (name: string) => ResourceSchema | undefined;
}

Behavior:

  • Fetches the schema on mount from GET /admin/schema
  • Caches the schema for 5 minutes — subsequent calls return the cached version
  • Falls back to the cached schema if a refresh fails
  • Updates the session store via sessionStore.updateSchema()
  • Requires an active session with a valid token

Example:

import { useAdminSchema } from '@craft-easy/admin/hooks';

function ResourceList() {
  const { schema, isLoading, error, getResource } = useAdminSchema();

  if (isLoading) return <ActivityIndicator />;
  if (error) return <Text>Error: {error}</Text>;

  const users = getResource('users');
  // users is a ResourceSchema with fields, list config, etc.
}

useResourceList

Paginated, sortable, filterable list of documents for a resource.

function useResourceList(
  resource: ResourceSchema,
  options?: UseResourceListOptions
): {
  items: any[];
  total: number;
  page: number;
  perPage: number;
  sort: string;
  filters: Record<string, string>;
  isLoading: boolean;
  error: string | null;
  setPage: (page: number) => void;
  setSort: (sort: string) => void;
  setFilters: (filters: Record<string, string>) => void;
  refresh: () => Promise<void>;
  totalPages: number;
}

interface UseResourceListOptions {
  page?: number;
  perPage?: number;
  sort?: string;
  filters?: Record<string, string>;
}

Behavior:

  • Calls GET {resource.endpoint} with page, per_page, sort, and filter parameters
  • Response type: { items: T[], total: number, page: number, per_page: number }
  • totalPages is computed as Math.max(1, Math.ceil(total / perPage))
  • Refetches automatically when page, sort, or filters change

Example:

import { useResourceList } from '@craft-easy/admin/hooks';

function UserList({ resource }: { resource: ResourceSchema }) {
  const {
    items, total, page, totalPages,
    sort, setSort, setPage, isLoading
  } = useResourceList(resource, { perPage: 25 });

  return (
    <View>
      <Text>{total} users found</Text>
      {items.map(user => (
        <Text key={user.id}>{user.name}</Text>
      ))}
      <Button title="Next" onPress={() => setPage(page + 1)} />
    </View>
  );
}

useResource

Fetches a single document by ID with ETag support.

function useResource(
  endpoint: string,
  id: string
): {
  data: any | null;
  etag: string | null;
  isLoading: boolean;
  error: string | null;
  refresh: () => Promise<void>;
}

Behavior:

  • Calls GET {endpoint}/{id}
  • Extracts the ETag response header for concurrency control
  • The etag value is passed to useResourceMutations for safe updates

Example:

import { useResource } from '@craft-easy/admin/hooks';

function UserDetail({ id }: { id: string }) {
  const { data, etag, isLoading, error, refresh } = useResource('/users', id);

  if (isLoading) return <ActivityIndicator />;

  return (
    <View>
      <Text>{data.name}</Text>
      <Text>ETag: {etag}</Text>
      <Button title="Refresh" onPress={refresh} />
    </View>
  );
}

useResourceMutations

Create, update, and delete operations with ETag concurrency.

function useResourceMutations(endpoint: string): {
  isLoading: boolean;
  error: string | null;
  createResource: (data: Record<string, any>) => Promise<any>;
  updateResource: (id: string, data: Record<string, any>, etag: string) => Promise<any>;
  deleteResource: (id: string, etag: string) => Promise<void>;
}

Behavior:

  • createResource — sends POST {endpoint} with JSON body, returns the created document
  • updateResource — sends PATCH {endpoint}/{id} with If-Match: {etag} header. Throws ApiError(412) if the document was modified since the ETag was fetched
  • deleteResource — sends DELETE {endpoint}/{id} with If-Match: {etag} header
  • All methods catch ApiError and store the message in error

Example:

import { useResource, useResourceMutations } from '@craft-easy/admin/hooks';

function EditUser({ id }: { id: string }) {
  const { data, etag, refresh } = useResource('/users', id);
  const { updateResource, isLoading, error } = useResourceMutations('/users');

  const handleSave = async (formData: Record<string, any>) => {
    try {
      await updateResource(id, formData, etag!);
      // Success — navigate back
    } catch (err) {
      if (err instanceof ApiError && err.status === 412) {
        // ETag mismatch — document was modified by someone else
        // Show collision modal (see Cross-Platform)
      }
    }
  };

  return (
    <View>
      {error && <Text style={{ color: 'red' }}>{error}</Text>}
      {/* Form fields here */}
      <Button title="Save" onPress={() => handleSave({ name: 'Updated' })} />
    </View>
  );
}

API Client

The hooks use an internal API client that handles authentication, ETag headers, and error parsing:

function createApiClient(sessionId?: string): {
  get: <T>(path: string) => Promise<ApiResponse<T>>;
  post: <T>(path: string, body: any) => Promise<ApiResponse<T>>;
  patch: <T>(path: string, body: any, etag: string) => Promise<ApiResponse<T>>;
  put: <T>(path: string, body: any, etag: string) => Promise<ApiResponse<T>>;
  delete: (path: string, etag: string) => Promise<ApiResponse>;
  list: <T>(path: string, params?: Record<string, string>) => Promise<ApiResponse<ListResponse<T>>>;
}
  • Automatically attaches the session token as X-API-Key header
  • PATCH/PUT/DELETE send the If-Match header with the ETag
  • Non-2xx responses throw ApiError(status, message, detail)