Skip to content

React hooks

Reference for @media-sdk/react@^0.3.0. All hooks except MediaProvider must be called inside a MediaProvider tree.

bash
pnpm add @media-sdk/core@^0.3.0 @media-sdk/react@^0.3.0 react

MediaProvider

Wraps your application (or a subtree) and supplies a MediaClient via React context.

Props

PropTypeRequiredDescription
clientMediaClientYesAny MediaClient implementation (PexelsMediaClient, PixabayMediaClient, or custom)
childrenReactNodeYesChild components that call hooks

Example

tsx
import { MediaProvider } from "@media-sdk/react";
import {
  ApiKeyProvider,
  PexelsMediaClient,
} from "@media-sdk/core";

const client = new PexelsMediaClient(
  new ApiKeyProvider(import.meta.env.VITE_PEXELS_API_KEY),
);

export function AppRoot({ children }: { children: React.ReactNode }) {
  return (
    <MediaProvider client={client}>
      {children}
    </MediaProvider>
  );
}

When you swap the client prop (for example, switching from Pexels to Pixabay at runtime), hooks re-subscribe and useMediaCapabilities() returns the new provider's capabilities.


useMediaClient

Returns the MediaClient instance from the nearest MediaProvider.

Returns

The hook returns the client directly (not wrapped in an object):

TypeDescription
MediaClientShared client for direct API calls (searchPhotos, trackView, etc.)

Example

tsx
import { useMediaClient } from "@media-sdk/react";

function TrackViewButton({ photoId }: { photoId: number }) {
  const client = useMediaClient();

  return (
    <button
      type="button"
      onClick={() =>
        void client.trackView({ mediaId: photoId, mediaType: "photo" })
      }
    >
      Track view
    </button>
  );
}

Errors

Throws if called outside MediaProvider:

useMediaClient must be used inside a MediaProvider

useMediaCapabilities

Returns MediaCapabilities for the active client via getCapabilities(client) from @media-sdk/core.

Returns

MediaCapabilities with three groups:

GroupFieldsUse for
operationssearchPhotos, searchVideos, getPhoto, getVideo, curatedPhotos, trackView, trackDownloadGate tabs, buttons, and client method calls
photoFiltersorientation, size, color, category, minWidth, minHeight, editorsChoice, localeGate photo filter UI
videoFiltersSame fields as photoFiltersGate video filter UI

Custom clients without a capabilities property receive an all-false fallback from getCapabilities.

Example

tsx
import { useMediaCapabilities } from "@media-sdk/react";

function FilterBar({
  orientation,
  onOrientationChange,
}: {
  orientation: string;
  onOrientationChange: (value: string) => void;
}) {
  const caps = useMediaCapabilities();

  return (
    <>
      {caps.photoFilters.orientation && (
        <select
          value={orientation}
          onChange={(event) => onOrientationChange(event.target.value)}
        >
          <option value="">Any orientation</option>
          <option value="landscape">Landscape</option>
          <option value="portrait">Portrait</option>
          <option value="square">Square</option>
        </select>
      )}

      {caps.photoFilters.size && (
        <select>
          <option value="large">Large</option>
          <option value="medium">Medium</option>
          <option value="small">Small</option>
        </select>
      )}
    </>
  );
}

Anti-pattern

Do not branch on provider identity:

tsx
// ❌ Forbidden
if (provider === "pexels") {
  showCuratedTab();
}

// ✅ Required
const caps = useMediaCapabilities();
if (caps.operations.curatedPhotos) {
  showCuratedTab();
}

useMediaSearch

Search photos with automatic fetching, pagination, race protection, and abort handling.

Options

Extends SearchParams from @media-sdk/core plus enabled:

OptionTypeDefaultDescription
querystringRequired. Search query. Empty or whitespace-only strings skip automatic requests
pagenumber1Initial page only — does not re-sync after mount
perPagenumberProvider defaultResults per page
enabledbooleantrueWhen false, skips automatic fetch on mount and when query changes
photoFiltersPhotoSearchFiltersForwarded to client.searchPhotos unchanged

photoFilters are not stripped or validated by the hook. Unsupported filters throw MediaError with code: "UNSUPPORTED_FILTER" from core. Gate filter UI with useMediaCapabilities().photoFilters before passing values.

PhotoSearchFilters

ts
interface PhotoSearchFilters {
  orientation?: "landscape" | "portrait" | "square";
  size?: "large" | "medium" | "small";
  color?: PhotoColor | PixabayColor;
  category?: string;
  minWidth?: number;
  minHeight?: number;
  editorsChoice?: boolean;
  locale?: string;
}

Returns

FieldTypeDescription
dataPaginatedResponse<Photo> | nullSearch results, or null before the first successful response
loadingbooleantrue while a request is in flight
errorError | nullLast non-abort error. Aborted requests do not set error
search() => Promise<void>Manually trigger a search (respects in-flight deduplication)
nextPage() => Promise<void>Navigate to the next page when data.pagination.hasNext is true
previousPage() => Promise<void>Navigate to the previous page when data.pagination.hasPrevious is true
refetch() => Promise<void>Re-run search for the current page, bypassing in-flight deduplication

Example

tsx
import { useMediaSearch } from "@media-sdk/react";

function PhotoSearch({ query }: { query: string }) {
  const {
    data,
    loading,
    error,
    nextPage,
    previousPage,
    refetch,
  } = useMediaSearch({
    query,
    page: 1,
    perPage: 20,
    enabled: true,
  });

  if (loading) {
    return <p>Loading…</p>;
  }

  if (error) {
    return <p>{error.message}</p>;
  }

  return (
    <div>
      <ul>
        {data?.items.map((photo) => (
          <li key={photo.id}>{photo.photographer}</li>
        ))}
      </ul>
      <button
        type="button"
        onClick={() => void previousPage()}
        disabled={!data?.pagination.hasPrevious}
      >
        Previous
      </button>
      <button
        type="button"
        onClick={() => void nextPage()}
        disabled={!data?.pagination.hasNext}
      >
        Next
      </button>
      <button type="button" onClick={() => void refetch()}>
        Refetch
      </button>
    </div>
  );
}

Filter forwarding

Pass photoFilters directly; the hook forwards them to core without modification:

tsx
import { useMediaCapabilities, useMediaSearch } from "@media-sdk/react";

function FilteredSearch({ query }: { query: string }) {
  const caps = useMediaCapabilities();

  const { data, loading } = useMediaSearch({
    query,
    photoFilters: {
      ...(caps.photoFilters.orientation
        ? { orientation: "landscape" as const }
        : {}),
      ...(caps.photoFilters.color
        ? { color: "blue" as const }
        : {}),
    },
  });

  if (loading) return <p>Loading…</p>;
  return <p>{data?.items.length ?? 0} results</p>;
}

Changing photoFilters triggers a new search (same as query or page changes).

enabled

Defer fetching until a condition is met — for example, after the user submits a search form:

tsx
import { useState } from "react";
import { useMediaSearch } from "@media-sdk/react";

function DeferredSearch() {
  const [query, setQuery] = useState("");
  const [submittedQuery, setSubmittedQuery] = useState("");

  const { data, loading, search } = useMediaSearch({
    query: submittedQuery,
    enabled: submittedQuery.length > 0,
  });

  function handleSubmit(event: React.FormEvent) {
    event.preventDefault();
    setSubmittedQuery(query);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <button type="submit">Search</button>
      {loading && <p>Loading…</p>}
      {data && <p>{data.items.length} results</p>}
    </form>
  );
}

When enabled is false:

  • No automatic fetch on mount
  • No automatic fetch when query changes
  • search() still works for manual triggers

Pagination

Read pagination from data.pagination:

tsx
const { data } = useMediaSearch({ query: "nature" });

const page = data?.pagination.page ?? 1;
const hasNext = data?.pagination.hasNext ?? false;
const hasPrevious = data?.pagination.hasPrevious ?? false;

Pass these values to Pagination from @media-sdk/ui-react.

Behavior

  • Empty or whitespace query → no automatic request
  • nextPage() / previousPage() check pagination.hasNext / pagination.hasPrevious and navigate by numeric page ± 1
  • refetch() re-runs the search for the current page, bypassing in-flight deduplication
  • In-flight requests are aborted when query, page, perPage, or filters change, on refetch(), or on unmount

useMediaVideos

Same contract as useMediaSearch, but calls client.searchVideos.

Options

OptionTypeDefaultDescription
querystringRequired. Search query
pagenumber1Initial page only
perPagenumberProvider defaultResults per page
enabledbooleantrueDefer automatic fetching when false
videoFiltersVideoSearchFiltersForwarded to searchVideos unchanged

VideoSearchFilters

ts
interface VideoSearchFilters {
  orientation?: "landscape" | "portrait" | "square";
  size?: "large" | "medium" | "small";
  category?: string;
  minWidth?: number;
  minHeight?: number;
  editorsChoice?: boolean;
  locale?: string;
}

Returns

Same shape as useMediaSearch but with PaginatedResponse<Video>:

FieldType
dataPaginatedResponse<Video> | null
loadingboolean
errorError | null
search() => Promise<void>
nextPage() => Promise<void>
previousPage() => Promise<void>
refetch() => Promise<void>

Example

tsx
import { useMediaVideos } from "@media-sdk/react";

function VideoSearch({ query }: { query: string }) {
  const {
    data,
    loading,
    error,
    nextPage,
    previousPage,
    refetch,
  } = useMediaVideos({
    query,
    perPage: 20,
    videoFilters: {
      orientation: "landscape",
      size: "medium",
    },
  });

  if (loading) return <p>Loading…</p>;
  if (error) return <p>{error.message}</p>;

  return (
    <ul>
      {data?.items.map((video) => (
        <li key={video.id}>{video.duration}s</li>
      ))}
    </ul>
  );
}

videoFilters are forwarded to @media-sdk/core unchanged. Check useMediaCapabilities().videoFilters before sending provider-specific filters.


Error handling

Hooks surface errors from @media-sdk/core as native Error instances. HTTP failures are MediaError with a status field; unsupported operations and filters use error.code.

See Error handling recipes for the full decision tree and copy-paste examples.

tsx
import { isAbortError, MediaError } from "@media-sdk/core";

function handleSearchError(error: Error) {
  if (isAbortError(error)) {
    // Request was cancelled — hooks swallow aborts in UI state
    return;
  }

  if (error instanceof MediaError) {
    if (error.code === "UNSUPPORTED_FILTER") {
      console.error("Filter not supported by this provider");
    } else if (error.code === "UNSUPPORTED_CAPABILITY") {
      console.error("Operation not supported by this provider");
    } else if (error.code === "INVALID_FILTER_VALUE") {
      console.error("Invalid filter value");
    } else {
      console.error(error.status, error.message);
    }
    return;
  }

  console.error(error.message);
}

Error codes

CodeWhen
UNSUPPORTED_CAPABILITYClient method not available on the active provider (e.g. getCuratedPhotos on Pixabay)
UNSUPPORTED_FILTERFilter field not supported by the active provider
INVALID_FILTER_VALUEFilter value not accepted (e.g. unsupported color or orientation)

Branch on error.code, not error.message. HTTP messages are provider-neutral in v0.3.0.

Aborts

Aborted requests do not set error. Hooks silently ignore aborts and prevent stale responses from overwriting newer state via a request-id guard.

Hooks abort in-flight requests when:

  • A new search starts (query, page, perPage, or filters change)
  • refetch() is called while a request is active
  • The component unmounts

See Cancellation for details.

Type exports

ts
import type {
  UseMediaSearchOptions,
  UseMediaSearchResult,
  UseMediaVideosOptions,
  UseMediaVideosResult,
} from "@media-sdk/react";

useMediaCapabilities re-exports MediaCapabilities from @media-sdk/core.