@media-sdk/react
v1.0.0
Published
React integration layer for @media-sdk/core
Readme
@media-sdk/react
React integration layer for @media-sdk/core. Provides a context provider and hooks for photo/video search, pagination, and client access in React applications.
Installation
pnpm add @media-sdk/core @media-sdk/react reactPeer Dependencies
- React 18 or React 19
@media-sdk/core is installed automatically as a runtime dependency.
Provider
Create a PexelsMediaClient from core, then wrap your application with MediaProvider:
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>
);
}useMediaClient
Access the shared client from context. Must be called inside MediaProvider:
import { useMediaClient } from "@media-sdk/react";
function Example() {
const client = useMediaClient();
// client.searchPhotos(), client.getPhoto(), etc.
}useMediaCapabilities
Read the active client's capabilities from context. Returns MediaCapabilities via getCapabilities(client):
import { useMediaCapabilities } from "@media-sdk/react";
function SearchTabs() {
const caps = useMediaCapabilities();
return (
<>
<Tab>Search</Tab>
{caps.operations.curatedPhotos && <Tab>Curated</Tab>}
</>
);
}Guard filter UI with caps.photoFilters / caps.videoFilters before passing values to search hooks.
useMediaSearch
Search photos with automatic fetching, pagination, and race protection:
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>
);
}Options
{
query: string;
page?: number; // initial page only (default: 1)
perPage?: number;
enabled?: boolean; // default: true
photoFilters?: PhotoSearchFilters; // forwarded to searchPhotos unchanged
}photoFilters are passed through to @media-sdk/core without hook-level stripping. Check useMediaCapabilities().photoFilters before sending provider-specific filters.
Result
{
data: PaginatedResponse<Photo> | null;
loading: boolean;
error: Error | null;
search: () => Promise<void>;
nextPage: () => Promise<void>;
previousPage: () => Promise<void>;
refetch: () => Promise<void>;
}Behavior
- Empty or whitespace
query→ no automatic request enabled: false→ skips automatic fetch;search()still works manuallynextPage()/previousPage()checkpagination.hasNext/pagination.hasPreviousand navigate by numeric page ± 1refetch()re-runs the search for the current page, bypassing in-flight deduplicationoptions.pageinitializes the hook's page state but does not re-sync after mount
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 presentational components such as @media-sdk/ui-react's Pagination.
enabled
Defer fetching until a condition is met (for example, after the user submits a search):
const [submittedQuery, setSubmittedQuery] = useState("");
const { data, loading, search } = useMediaSearch({
query: submittedQuery,
enabled: submittedQuery.length > 0,
});
function handleSubmit(query: string) {
setSubmittedQuery(query);
}When enabled is false, the hook does not auto-fetch on mount or when query changes. Call search() manually to trigger a request.
useMediaVideos
Same contract as useMediaSearch, but for videos:
import { useMediaVideos } from "@media-sdk/react";
const {
data,
loading,
error,
nextPage,
previousPage,
refetch,
} = useMediaVideos({
query: "nature",
perPage: 20,
videoFilters: {
orientation: "landscape",
size: "medium",
},
});videoFilters are forwarded to searchVideos unchanged.
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:
import { isAbortError, MediaError } from "@media-sdk/core";
if (error instanceof MediaError) {
if (error.code === "UNSUPPORTED_FILTER") {
} else if (error.code === "UNSUPPORTED_CAPABILITY") {
} else {
console.error(error.status, error.message);
}
}
if (isAbortError(error)) {
// request was cancelled — hooks swallow aborts in UI state
}Aborted requests do not set error — see cancellation below.
Cancellation
Hooks abort in-flight requests when:
- A new search starts (query, page, or
perPagechanges) refetch()is called while a request is active- The component unmounts
Aborted requests are silently ignored: error stays null and stale responses cannot overwrite newer state. The latest request always wins via a request-id guard.
Architecture
React Application
│
▼
@media-sdk/react ← Provider + hooks
│
▼
@media-sdk/core ← API client, cache, types
│
▼
Pexels API@media-sdk/react does not own API transport, cache, or HTTP logic. Pair it with @media-sdk/ui-react for presentational components and keep orchestration in your application layer.
Public API
import {
MediaProvider,
useMediaCapabilities,
useMediaClient,
useMediaSearch,
useMediaVideos,
type UseMediaSearchOptions,
type UseMediaSearchResult,
type UseMediaVideosOptions,
type UseMediaVideosResult,
} from "@media-sdk/react";MediaContext is internal and not part of the public API.
