Skip to content

React Native integration

@media-sdk/native is the React Native hooks layer for the Headless Media SDK. It provides MediaProvider and hooks for photo and video search, pagination, capability discovery, and client access — without owning HTTP transport, caching, or presentational UI.

bash
pnpm add @media-sdk/core@^0.3.1 @media-sdk/native@^0.1.0 react

Peer dependency: React 18 or 19. @media-sdk/core is installed automatically as a runtime dependency. The hooks package does not require Expo or react-native as a peer — only React.

Two packages for React Native

PackageRole
@media-sdk/nativeHooks + MediaProvider — search, pagination, capabilities
@media-sdk/ui-nativePresentational RN components (PhotoGrid, SearchBar, Modal previews, …)

Both depend on @media-sdk/core only. Your app wires hooks to UI components (or builds custom UI).

Your React Native app

       ├── @media-sdk/native      ← MediaProvider + hooks
       │         │
       │         ▼
       │   @media-sdk/core        ← MediaClient, types, cache

       └── @media-sdk/ui-native   ← presentational components (optional)

@media-sdk/ui-native peers react-native ≥ 0.71 (global fetch). @media-sdk/native does not — you can use hooks in any React renderer that provides context.

Expo vs bare React Native

The SDK packages are Expo-agnostic. No expo imports in @media-sdk/native or @media-sdk/ui-native.

LayerExpo required?
@media-sdk/coreNo
@media-sdk/nativeNo
@media-sdk/ui-nativeNo
Your appYour choice — Expo example or bare RN CLI

Quick setup

Create a client from core, wrap your app with MediaProvider, then use hooks in child components:

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

const client = new PexelsMediaClient(
  new ApiKeyProvider(process.env.EXPO_PUBLIC_PEXELS_API_KEY!),
);

export function AppRoot({ children }: { children: React.ReactNode }) {
  return (
    <MediaProvider client={client}>
      {children}
    </MediaProvider>
  );
}
tsx
import { useMediaSearch } from "@media-sdk/native";
import { Text, View } from "react-native";

function PhotoSearch() {
  const { data, loading, error } = useMediaSearch({
    query: "nature",
    perPage: 20,
  });

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

  return (
    <View>
      {data?.items.map((photo) => (
        <Text key={photo.id}>{photo.photographer}</Text>
      ))}
    </View>
  );
}

Public API (@media-sdk/native)

ts
import {
  MediaProvider,
  useMediaCapabilities,
  useMediaClient,
  useMediaSearch,
  useMediaVideos,
  type MediaCapabilities,
  type UseMediaSearchOptions,
  type UseMediaSearchResult,
  type UseMediaVideosOptions,
  type UseMediaVideosResult,
} from "@media-sdk/native";

Internal hooks (usePaginatedMediaSearch, useSearchAbortController) are not exported.

Hooks overview

HookPurpose
MediaProviderSupplies a MediaClient to the React tree
useMediaClientRead the shared client from context
useMediaCapabilitiesRead provider capabilities for UI gating
useMediaSearchPhoto search with loading, error, and pagination
useMediaVideosVideo search with the same contract

Hook contracts mirror @media-sdk/react — same options, return shapes, filter forwarding, and abort behavior.

Capability-driven UI

Gate features with useMediaCapabilities, not provider name strings:

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

function SearchTabs() {
  const caps = useMediaCapabilities();

  return (
    <>
      {/* Photos tab always available when searchPhotos is true */}
      {caps.operations.searchPhotos && <Tab label="Search" />}
      {caps.operations.curatedPhotos && <Tab label="Curated" />}
      {caps.operations.searchVideos && <Tab label="Videos" />}
    </>
  );
}

See Capabilities and Filters.

Wiring hooks and UI

tsx
import { useMediaSearch } from "@media-sdk/native";
import {
  SearchBar,
  PhotoGrid,
  Pagination,
  LoadingState,
  ErrorState,
} from "@media-sdk/ui-native";
import { useState } from "react";

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

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

  return (
    <>
      <SearchBar
        value={query}
        onChange={setQuery}
        onSubmit={() => setSubmittedQuery(query)}
      />

      {loading && <LoadingState message="Loading photos…" />}
      {error && <ErrorState message={error.message} />}

      {data && (
        <>
          <PhotoGrid photos={data.items} onPhotoSelect={() => {}} />
          <Pagination
            page={data.pagination.page}
            hasPrevious={data.pagination.hasPrevious}
            hasNext={data.pagination.hasNext}
            loading={loading}
            accessibilityLabel="Photo results"
            onPrevious={() => void previousPage()}
            onNext={() => void nextPage()}
          />
        </>
      )}
    </>
  );
}

Next steps