This guide covers React-specific integration. For component properties and events, see Client Packages.
Ensure the GitHub package registry is configured (see Registry Setup), then install both packages:
pnpm add @edufeed-org/oer-finder-plugin @edufeed-org/oer-finder-plugin-reactThe base plugin is a peer dependency of the React package. Both packages must be installed — the React package provides the React component wrappers, while the base plugin provides the Web Components, adapter registry, and adapter entry points.
The plugin supports server-proxy mode (with apiUrl prop) and direct client mode (without apiUrl). For full details on each mode, adapter registration, and available adapters, see Client Packages — Routing Modes and Available Adapters.
- Server-proxy mode: Set
apiUrl— no adapter registration needed, no adapter code in your bundle. - Direct client mode: Omit
apiUrl— register adapters at your app's entry point before the component renders. Import adapter registration functions from@edufeed-org/oer-finder-plugin.
The recommended pattern is to slot OerList and OerLoadMore inside OerSearch. Below are complete examples for each mode.
import { useState, useCallback } from 'react';
import {
OerSearch,
OerList,
OerLoadMore,
type OerSearchResultEvent,
type OerCardClickEvent,
type OerItem,
type LoadMoreMeta,
type SourceConfig,
} from '@edufeed-org/oer-finder-plugin-react';
// Configure available sources (checked: true sets the pre-selected sources)
const SOURCES: SourceConfig[] = [
{ id: 'nostr-amb-relay', label: 'Nostr AMB Relay', checked: true },
{ id: 'openverse', label: 'Openverse' },
{ id: 'arasaac', label: 'ARASAAC' },
{ id: 'rpi-virtuell', label: 'RPI-Virtuell' },
{ id: 'wikimedia', label: 'Wikimedia Commons' },
];
function OerFinder() {
const [oers, setOers] = useState<OerItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [metadata, setMetadata] = useState<LoadMoreMeta | null>(null);
const handleSearchLoading = useCallback(() => {
setLoading(true);
}, []);
const handleSearchResults = useCallback(
(event: OerSearchResultEvent) => {
const { data, meta } = event.detail;
setOers(data);
setLoading(false);
setError(null);
setMetadata(meta);
},
[],
);
const handleSearchError = useCallback(
(event: CustomEvent<{ error: string }>) => {
setOers([]);
setLoading(false);
setError(event.detail.error);
setMetadata(null);
},
[],
);
const handleSearchCleared = useCallback(() => {
setOers([]);
setLoading(false);
setError(null);
setMetadata(null);
}, []);
const handleCardClick = useCallback(
(event: OerCardClickEvent) => {
const oer = event.detail.oer;
const url = oer.amb?.id;
if (url) {
window.open(String(url), '_blank', 'noopener,noreferrer');
}
},
[],
);
// Note: load-more events bubble up and are automatically
// caught by OerSearch to fetch the next page of results.
return (
<div>
<OerSearch
apiUrl="https://your-api-url.com"
language="en"
pageSize={20}
sources={SOURCES}
onSearchLoading={handleSearchLoading}
onSearchResults={handleSearchResults}
onSearchError={handleSearchError}
onSearchCleared={handleSearchCleared}
>
<OerList
oers={oers}
loading={loading}
error={error}
language="en"
onCardClick={handleCardClick}
/>
<OerLoadMore metadata={metadata} loading={loading} language="en" />
</OerSearch>
</div>
);
}The component code is identical to the server-proxy example above with two differences: adapters must be registered at startup, and the apiUrl prop is omitted.
1. Register adapters once at your app entry point (e.g., main.tsx):
import { registerOpenverseAdapter } from '@edufeed-org/oer-finder-plugin/adapter/openverse';
import { registerArasaacAdapter } from '@edufeed-org/oer-finder-plugin/adapter/arasaac';
registerOpenverseAdapter();
registerArasaacAdapter();2. Render OerSearch without apiUrl:
<OerSearch
language="en"
pageSize={20}
sources={SOURCES}
onSearchLoading={handleSearchLoading}
onSearchResults={handleSearchResults}
onSearchError={handleSearchError}
onSearchCleared={handleSearchCleared}
>
<OerList oers={oers} loading={loading} error={error} language="en" onCardClick={handleCardClick} />
<OerLoadMore metadata={metadata} loading={loading} language="en" />
</OerSearch>All state management, event handlers, and child components remain the same.
OerSearch acts as the orchestrator. When you render OerList and OerLoadMore as children of OerSearch, they are slotted into the underlying <oer-search> web component. This enables automatic event bubbling: the load-more event fired by OerLoadMore bubbles up through the DOM and is caught by the parent OerSearch, which then fetches the next page of results and emits a new search-results event. You do not need to wire up any load-more handler yourself — just place the components in the correct parent-child relationship:
<OerSearch /* handles pagination automatically */>
<OerList /* displays results */ />
<OerLoadMore /* fires load-more, caught by OerSearch */ />
</OerSearch>If you render OerLoadMore outside of OerSearch, the event will not bubble to the search component and pagination will not work automatically. In that case you would need to handle the onLoadMore event yourself.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
apiUrl |
string |
No | — | Base URL of the OER Proxy API. When provided, activates server-proxy mode. When omitted, activates direct client mode (adapters must be registered). |
sources |
SourceConfig[] |
No | [openverse, arasaac] |
Available sources shown in the UI. See Source Configuration. |
language |
SupportedLanguage |
No | 'en' |
UI language ('en' or 'de'). |
pageSize |
number |
No | 20 |
Number of results per page. |
lockedType |
string |
No | — | Lock the type filter to a specific value (e.g., 'image'). |
showTypeFilter |
boolean |
No | true |
Show or hide the type filter dropdown. |
lockedSource |
string |
No | — | Lock the source filter to a specific value. |
showSourceFilter |
boolean |
No | true |
Show or hide the source filter checkboxes. |
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
oers |
OerItem[] |
No | [] |
Array of OER items to display. |
loading |
boolean |
No | false |
When true, shows a loading skeleton. |
error |
string | null |
No | null |
Error message to display. Pass null to clear. |
language |
SupportedLanguage |
No | 'en' |
UI language ('en' or 'de'). |
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
oer |
OerItem | null |
No | null |
OER item data to render. |
language |
SupportedLanguage |
No | 'en' |
UI language ('en' or 'de'). |
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
metadata |
LoadMoreMeta | null |
No | null |
Pagination metadata ({ total, shown, hasMore }). Controls the "Showing X of Y" indicator and whether the button is visible. |
loading |
boolean |
No | false |
When true, disables the button and shows a loading state. |
language |
SupportedLanguage |
No | 'en' |
UI language ('en' or 'de'). |
| Prop | Callback Signature | Description |
|---|---|---|
onSearchLoading |
(event: CustomEvent<void>) => void |
Fired when a search request starts. Use this to set loading state. |
onSearchResults |
(event: OerSearchResultEvent) => void |
Fired when search completes. event.detail contains { data: OerItem[], meta: LoadMoreMeta }. |
onSearchError |
(event: CustomEvent<{ error: string }>) => void |
Fired when a search fails. event.detail.error contains the error message. |
onSearchCleared |
(event: CustomEvent<void>) => void |
Fired when the user clears the search input. |
| Prop | Callback Signature | Description |
|---|---|---|
onCardClick |
(event: OerCardClickEvent) => void |
Fired when a card is clicked. event.detail.oer contains the clicked OerItem. Bubbles up from child OerCard components. |
| Prop | Callback Signature | Description |
|---|---|---|
onCardClick |
(event: OerCardClickEvent) => void |
Fired when the card image is clicked. event.detail.oer contains the OerItem. |
| Prop | Callback Signature | Description |
|---|---|---|
onLoadMore |
(event: CustomEvent<void>) => void |
Fired when the "Load more" button is clicked. When slotted inside OerSearch, this event bubbles up automatically to trigger the next page fetch — no manual handler needed. |
The React package (@edufeed-org/oer-finder-plugin-react) re-exports all shared types from the base plugin, so you can import everything from a single package. For the full list of shared types, see Key Types.
import {
// React wrapper components
OerSearch,
OerList,
OerCard,
OerLoadMore,
// All shared types are re-exported (OerItem, SourceConfig, event types, etc.)
type OerSearchResultEvent,
type OerCardClickEvent,
type OerItem,
type LoadMoreMeta,
type SourceConfig,
// ... see Client Packages — Key Types for the complete list
} from '@edufeed-org/oer-finder-plugin-react';Adapter registry API and adapter registration functions are imported from @edufeed-org/oer-finder-plugin:
import {
registerAdapter,
getAdapterFactory,
type AdapterFactory,
} from '@edufeed-org/oer-finder-plugin';When managing component state, use explicit generic parameters:
const [oers, setOers] = useState<OerItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [metadata, setMetadata] = useState<LoadMoreMeta | null>(null);The React wrapper uses camelCase props that map to web component attributes:
| React Prop | Web Component Attribute / Property |
|---|---|
apiUrl |
api-url |
pageSize |
page-size |
sources |
sources (JS property) |
lockedType |
locked-type |
showTypeFilter |
show-type-filter |
lockedSource |
locked-source |
showSourceFilter |
show-source-filter |
onSearchLoading |
search-loading event |
onSearchResults |
search-results event |
onSearchError |
search-error event |
onSearchCleared |
search-cleared event |
onCardClick |
card-click event |
onLoadMore |
load-more event |
See packages/oer-finder-plugin-react-example for a complete working example.