React hooks
Reference for @media-sdk/react@^0.3.0. All hooks except MediaProvider must be called inside a MediaProvider tree.
pnpm add @media-sdk/core@^0.3.0 @media-sdk/react@^0.3.0 reactMediaProvider
Wraps your application (or a subtree) and supplies a MediaClient via React context.
Props
| Prop | Type | Required | Description |
|---|---|---|---|
client | MediaClient | Yes | Any MediaClient implementation (PexelsMediaClient, PixabayMediaClient, or custom) |
children | ReactNode | Yes | Child components that call hooks |
Example
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):
| Type | Description |
|---|---|
MediaClient | Shared client for direct API calls (searchPhotos, trackView, etc.) |
Example
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 MediaProvideruseMediaCapabilities
Returns MediaCapabilities for the active client via getCapabilities(client) from @media-sdk/core.
Returns
MediaCapabilities with three groups:
| Group | Fields | Use for |
|---|---|---|
operations | searchPhotos, searchVideos, getPhoto, getVideo, curatedPhotos, trackView, trackDownload | Gate tabs, buttons, and client method calls |
photoFilters | orientation, size, color, category, minWidth, minHeight, editorsChoice, locale | Gate photo filter UI |
videoFilters | Same fields as photoFilters | Gate video filter UI |
Custom clients without a capabilities property receive an all-false fallback from getCapabilities.
Example
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:
// ❌ 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:
| Option | Type | Default | Description |
|---|---|---|---|
query | string | — | Required. Search query. Empty or whitespace-only strings skip automatic requests |
page | number | 1 | Initial page only — does not re-sync after mount |
perPage | number | Provider default | Results per page |
enabled | boolean | true | When false, skips automatic fetch on mount and when query changes |
photoFilters | PhotoSearchFilters | — | Forwarded 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
interface PhotoSearchFilters {
orientation?: "landscape" | "portrait" | "square";
size?: "large" | "medium" | "small";
color?: PhotoColor | PixabayColor;
category?: string;
minWidth?: number;
minHeight?: number;
editorsChoice?: boolean;
locale?: string;
}Returns
| Field | Type | Description |
|---|---|---|
data | PaginatedResponse<Photo> | null | Search results, or null before the first successful response |
loading | boolean | true while a request is in flight |
error | Error | null | Last 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
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:
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:
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
querychanges search()still works for manual triggers
Pagination
Read pagination from data.pagination:
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()checkpagination.hasNext/pagination.hasPreviousand navigate by numeric page ± 1refetch()re-runs the search for the current page, bypassing in-flight deduplication- In-flight requests are aborted when
query,page,perPage, or filters change, onrefetch(), or on unmount
useMediaVideos
Same contract as useMediaSearch, but calls client.searchVideos.
Options
| Option | Type | Default | Description |
|---|---|---|---|
query | string | — | Required. Search query |
page | number | 1 | Initial page only |
perPage | number | Provider default | Results per page |
enabled | boolean | true | Defer automatic fetching when false |
videoFilters | VideoSearchFilters | — | Forwarded to searchVideos unchanged |
VideoSearchFilters
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>:
| Field | Type |
|---|---|
data | PaginatedResponse<Video> | null |
loading | boolean |
error | Error | null |
search | () => Promise<void> |
nextPage | () => Promise<void> |
previousPage | () => Promise<void> |
refetch | () => Promise<void> |
Example
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.
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
| Code | When |
|---|---|
UNSUPPORTED_CAPABILITY | Client method not available on the active provider (e.g. getCuratedPhotos on Pixabay) |
UNSUPPORTED_FILTER | Filter field not supported by the active provider |
INVALID_FILTER_VALUE | Filter 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
import type {
UseMediaSearchOptions,
UseMediaSearchResult,
UseMediaVideosOptions,
UseMediaVideosResult,
} from "@media-sdk/react";useMediaCapabilities re-exports MediaCapabilities from @media-sdk/core.