ra-search

react-admin ≥ 5.0.2

Plug your search engine and let users search across all resources via a smart Omnibox.

In large admins, users need several clicks to get to one record. For repetitive tasks, this ends up costing minutes every day. The ra-search Omnibox simplifies navigation by providing a global, always-on search engine for records.

It even remembers the previous searches, so that end-users can easily find the record they were looking for.

Ra-search can take advantage of a search index like ElasticSearch if you have one, or it can rely on your REST API by searching across multiple resources in parallel.

Test it live in the Enterprise Edition Storybook and in the e-commerce demo.

Installation

npm install --save @react-admin/ra-search
# or
yarn add @react-admin/ra-search

Tip: ra-search is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.

ra-search relies on the dataProvider to communicate with the search engine. Whenever a user enters a search query, react-admin calls dataProvider.search(query). So for ra-search to work, you must implement this method in your data provider.

Example Search Query

dataProvider.search('roll').then(response => console.log(response));
// {
//     data: [
//         { id: 'a7535', type: 'artist', url: '/artists/7535', content: { label: 'The Rolling Stones', description: 'English rock band formed in London in 1962'  } }
//         { id: 'a5352', type: 'artist', url: '/artists/5352', content: { label: 'Sonny Rollins', description: 'American jazz tenor saxophonist'  } }
//         { id: 't7524', type: 'track', url: '/tracks/7524', content: { label: 'Like a Rolling Stone', year: 1965, recordCompany: 'Columbia', artistId: 345, albumId: 435456 } }
//         { id: 't2386', type: 'track', url: '/tracks/2386', content: { label: "It's Only Rock 'N Roll (But I Like It)", year: 1974, artistId: 7535, albumId: 6325 } }
//         { id: 'a6325', type: 'album', url: '/albums/6325', content: { label: "It's Only rock 'N Roll", year: 1974, artistId: 7535 }}
//     ],
//     total: 5
// }
dataProvider.search("roll").then((response) => console.log(response));
// {
//     data: [
//         { id: 'a7535', type: 'artist', url: '/artists/7535', content: { label: 'The Rolling Stones', description: 'English rock band formed in London in 1962'  } }
//         { id: 'a5352', type: 'artist', url: '/artists/5352', content: { label: 'Sonny Rollins', description: 'American jazz tenor saxophonist'  } }
//         { id: 't7524', type: 'track', url: '/tracks/7524', content: { label: 'Like a Rolling Stone', year: 1965, recordCompany: 'Columbia', artistId: 345, albumId: 435456 } }
//         { id: 't2386', type: 'track', url: '/tracks/2386', content: { label: "It's Only Rock 'N Roll (But I Like It)", year: 1974, artistId: 7535, albumId: 6325 } }
//         { id: 'a6325', type: 'album', url: '/albums/6325', content: { label: "It's Only rock 'N Roll", year: 1974, artistId: 7535 }}
//     ],
//     total: 5
// }

Input and Output Formats

The dataProvider.search() method should return a Promise for data containing an array of SearchResult objects. A SearchResult contains at least the following fields:

  • id: Identifier The unique identifier of the search result
  • type: string An arbitrary string which enables grouping
  • url: string The URL where to redirect to on click. It could be a custom page and not a resource if you want to
  • content: any Can contain any data that will be used to display the result. If used with the default <SearchResultItem> component, it must contain at least an id, label, and a description.
  • matches: any An optional object containing an extract of the data with matches. Can be anything that will be interpreted by a <SearchResultItem>

As for the total, it can be greater than the number of returned results. This is useful e.g. to show that there are more results.

It is your responsibility to add this search method to your dataProvider so that react-admin can send queries to and read responses from the search engine.

TypeScript Types

type search = (
    query: string,
    options?: SearchOptions
) => Promise<{ data: SearchResult[]; total: number }>;

interface SearchOptions {
    targets?: string[];
    [key: string]: any;
}

interface SearchResult {
    id: Identifier;
    type: string;
    url: string;
    content: any;
    matches?: any;
}

addSearchMethod Helper

If you don't have a full-text search endpoint in your API, you can use the addSearchMethod() helper function. It adds a search() method to an existing dataProvider; this method calls dataProvider.getList() on several resources in parallel, and aggregates the result into a single response.

For example, to add a dataProvider.search() method that searches across the artists, albums, and tracks resources, you can do:

import simpleRestProvider from 'ra-data-simple-rest';
import { addSearchMethod } from '@react-admin/ra-search';

const dataProvider = simpleRestProvider('http://path.to.my.api/');

const dataProviderWithSearch = addSearchMethod(dataProvider, [
    'artists',
    'tracks',
    'albums',
]);

Calling dataProvider.search('roll') issues the following queries in parallel:

  • dataProvider.getList('artists', { filter: { q: "roll" }})
  • dataProvider.getList('tracks', { filter: { q: "roll" }})
  • dataProvider.getList('albumns', { filter: { q: "roll" }})

Then aggregate the results and return them in a single response.

We don't recommend using addSearchMethod() in production, because if there are many resources, the API may receive too many concurrent requests, and the generated dataProvider.search() method is as slow as the slowest dataProvider.getList() call. Instead, you should expose a search API endpoint, e.g. by using a search engine, and implement your own dataProvider.search() method to convert the results to the format expected by ra-search.

The second argument to addSearchMethod is the builder configuration. It can be either an array of resource names or a map of the resources specifying how to format their records for search results.

The (optional) third argument to addSearchMethod is the redirect option. It allows to choose whether the search results will redirect to the Show or the Edit page. You can also change this on a per-resource basis (see below). By default, search results redirect to the Edit page.

When called with an array of resources, addSearchMethod populates the search results content based on the records returned by dataProvider.getList(), with the following inference rules:

  • id: Returns the record id
  • label: Returns the record label or name or title
  • description: Returns the record description or body

Example with an array of resources:

const dataProviderWithSearch = addSearchMethod(dataProvider, [
    'artists',
    'albums',
]);

When called with a map, each key being a resource name, the value can have the following properties:

  • label: Either the field name to use as the label or a function that will be called with a record and must return a string. Defaults to the inference described above.
  • description: Either the field name to use as the description or a function that will be called with a record and must return a string. Defaults to the inference described above.
  • redirect: Optional. Argument that defines if the redirection is done on a show or edit page. In case the redirection is also defined globally, the per-resource one takes precedence.

Examples with a map of resources:

const dataProviderWithSearch = addSearchMethod(dataProvider, {
    artists: {
        label: 'full_name',
        description: record =>
            `${record.born_at}-${record.died_at} ${record.biography}`,
        redirect: 'show',
    },
    albums: {
        // no label specified, fallback on inference
        description: record =>
            `${record.released_at.getFullYear()} by ${record.recordCompany}`,
    },
});

const dataProviderWithSearchAndGlobalRedirection = addSearchMethod(
    dataProvider,
    {
        artists: {
            label: 'full_name',
            description: record =>
                `${record.born_at}-${record.died_at} ${record.biography}`,
        },
        albums: {
            // no label specified, fallback on inference
            description: record =>
                `${record.released_at.getFullYear()} by ${
                    record.recordCompany
                }`,
        },
    },
    'show'
);

The <Search> component includes an input and displays the search results inside an MUI Popover.

By default, it will group the search results by target, and show their content.label and content.description.

Usage

Include the <Search> component inside a custom <AppBar> component:

import { Admin, AppBar, TitlePortal, Layout } from 'react-admin';
import { Search } from '@react-admin/ra-search';

const MyAppBar = () => (
    <AppBar>
        <TitlePortal />
        <Search />
    </AppBar>
);

const MyLayout = props => <Layout {...props} appBar={MyAppBar} />;

export const App = () => (
    <Admin dataProvider={searchDataProvider} layout={MyLayout}>
        // ...
    </Admin>
);
import { Admin, AppBar, TitlePortal, Layout } from "react-admin";
import { Search } from "@react-admin/ra-search";

const MyAppBar = () => (
    <AppBar>
        <TitlePortal />
        <Search />
    </AppBar>
);

const MyLayout = (props) => <Layout {...props} appBar={MyAppBar} />;

export const App = () => (
    <Admin dataProvider={searchDataProvider} layout={MyLayout}>
        // ...
    </Admin>
);

Props

The <Search> component accepts the following props:

Prop Required Type Default Description
children Optional Element <SearchResultsPanel> A component that will display the results.
disableHighlight Optional boolean false Disable the highlight of the search term of each result.
historySize Optional number 5 The number of past queries to keep in history.
keyboardShortcutIcon Optional boolean or ReactElement <span>Ctrl+k</span> or <span>⌘+k</span> Disable or configure the keyboard shortcut icon.
isInAppBar Optional boolean true Apply a dedicated style to the <AppBar> if true.
options Optional Object - An object containing options to apply to the search.
queryOptions Optional UseQuery Options - react-query options for the search query.
withKeyboardShortcut Optional boolean or Keys false Enable or configure a shortcut to open and access the search input and their results.
wait Optional number 500 The delay of debounce for the search to launch after typing in ms.

Additional props are passed down to the Material UI <TextField> component.

children

The <Search> children allow you to customize the way results are displayed. The child component can grab the search result using the useSearchResult hook.

import { Admin, AppBar, TitlePortal, Layout } from 'react-admin';
import { Search, useSearchResult } from '@react-admin/ra-search';

const CustomSearchResultsPanel = () => {
    const { data, onClose } = useSearchResult();

    return (
        <ul>
            {data.map(searchResult => (
                <li key={searchResult.id}>{searchResult.content.label}</li>
            ))}
        </ul>
    );
};

const MyAppBar = () => (
    <AppBar>
        <TitlePortal />
        <Search>
            <CustomSearchResultsPanel />
        </Search>
    </AppBar>
);

const MyLayout = props => <Layout {...props} appBar={MyAppBar} />;

export const App = () => (
    <Admin dataProvider={searchDataProvider} layout={MyLayout}>
        // ...
    </Admin>
);
import { Admin, AppBar, TitlePortal, Layout } from "react-admin";
import { Search, useSearchResult } from "@react-admin/ra-search";

const CustomSearchResultsPanel = () => {
    const { data, onClose } = useSearchResult();

    return (
        <ul>
            {data.map((searchResult) => (
                <li key={searchResult.id}>{searchResult.content.label}</li>
            ))}
        </ul>
    );
};

const MyAppBar = () => (
    <AppBar>
        <TitlePortal />
        <Search>
            <CustomSearchResultsPanel />
        </Search>
    </AppBar>
);

const MyLayout = (props) => <Layout {...props} appBar={MyAppBar} />;

export const App = () => (
    <Admin dataProvider={searchDataProvider} layout={MyLayout}>
        // ...
    </Admin>
);

disableHighlight

The search terms in each result are highlighted. You can disable this feature with the disableHighlight prop as follows:

<Search disableHighlight />
<Search disableHighlight />;

Tip: To customize the highlight style check out the Customizing the result items section below.

historySize

The number of previous user searches to keep in the popover. For example, if a user performs 10 searches and historySize is set to 5, the popover will display the user's last 5 queries.

<Search historySize={5} />
<Search historySize={5} />;

keyboardShortcutIcon

Use the keyboardShortcutIcon prop to disable or customize the keyboard shortcut icon.

Keyboard Shortcut Icon

<Search keyboardShortcutIcon={<span>Shift+s</span>} />
<Search keyboardShortcutIcon={<span>Shift+s</span>} />;

If you don't want to display this icon, you can pass false to the keyboardShortcutIcon prop:

<Search keyboardShortcutIcon={false} />
<Search keyboardShortcutIcon={false} />;

isInAppBar

The <Search> component has a specific style to fit the admin appBar. If you need to render the <Search> component elsewhere, you can set isInAppBar to false and hence apply the default MUI style.

<Search isInAppBar={false} />
<Search isInAppBar={false} />;

options

An object containing options to apply to the search:

  • targets:string[]: an array of the indices on which to perform the search. Defaults to an empty array.
  • {any}:{any}: any custom option to pass to the search engine.
<Search options={{ foo: 'bar' }} />
<Search options={{ foo: "bar" }} />;

queryOptions

<Search> accepts a queryOptions prop to pass options to the react-query client. This can be useful e.g. to override the default side effects such as onSuccess or onError.

<Search queryOptions={{ onSuccess: data => console.log(data) }} />
<Search queryOptions={{ onSuccess: (data) => console.log(data) }} />;

withKeyboardShortcut

Enable opening and accessing the search input and their results with a keyboard shortcut.

Keyboard Shortcut Icon

<Search withKeyboardShortcut />
<Search withKeyboardShortcut />;

The default shortcut is Ctrl+K on Windows and Linux, and Cmd+K on MacOS. You can override this shortcut by passing the key combination as a string to the withKeyboardShortcut prop:

<Search withKeyboardShortcut="ctrl+f" />
<Search withKeyboardShortcut="ctrl+f" />;

You can also set multiple shortcuts:

<Search
    withKeyboardShortcut={['mod+k', 'shift+s']}
    helperText="Open search with 'ctrl+k', 'cmd+k' or 'shift+s'"
/>
<Search withKeyboardShortcut={["mod+k", "shift+s"]} helperText="Open search with 'ctrl+k', 'cmd+k' or 'shift+s'" />;

You can use the following modifiers and special keys:

  • shift
  • alt
  • ctrl
  • meta
  • mod (which listens for ctrl on Windows/Linux and cmd on macOS)
  • backspace
  • tab
  • clear
  • enter or return
  • esc or escape
  • space
  • up, down, left, right
  • pageup, pagedown
  • del or delete
  • f1, f2 ... f19

Tip: Upper case letters are considered lower case letters. To handle users pressing the letter S in capital letters, you must set withKeyboardShortcut to shift+s.

Tip: If you override the default shortcut with an array of multiple shortcuts, the default shortcut icon will be your first shortcut. To edit it, you can update the shortcut icon as well.

wait

The number of milliseconds to wait before processing the search request, immediately after the user enters their last character.

<Search wait={200} />
<Search wait={200} />;

Customizing the Entire Search Results

Pass a custom React element as a child of <Search> to customize the appearance of the search results. This can be useful e.g. to customize the results grouping, or to arrange search results differently.

ra-search renders the <Search> inside a SearchContext. You can use the useSearchResult hook to read the search results, as follows:

import { Search, useSearchResult } from '@react-admin/ra-search';

const MySearch = props => (
    <Search>
        <CustomSearchResultsPanel />
    </Search>
);

const CustomSearchResultsPanel = () => {
    const { data, onClose } = useSearchResult();

    return (
        <ul>
            {data.map(searchResult => (
                <li key={searchResult.id}>
                    <Link to={searchResult.url} onClick={onClose}>
                        <strong>{searchResult.content.label}</strong>
                    </Link>
                    <p>{searchResult.content.description}</p>
                </li>
            ))}
        </ul>
    );
};
import { Search, useSearchResult } from "@react-admin/ra-search";

const MySearch = (props) => (
    <Search>
        <CustomSearchResultsPanel />
    </Search>
);

const CustomSearchResultsPanel = () => {
    const { data, onClose } = useSearchResult();

    return (
        <ul>
            {data.map((searchResult) => (
                <li key={searchResult.id}>
                    <Link to={searchResult.url} onClick={onClose}>
                        <strong>{searchResult.content.label}</strong>
                    </Link>
                    <p>{searchResult.content.description}</p>
                </li>
            ))}
        </ul>
    );
};

Customizing The Result Items

By default, <Search> displays the results in <SearchResultsPanel>, which displays each results in a <SearchResultItem>. So rendering <Search> without children is equivalent to rendering:

const MySearch = () => (
    <Search>
        <SearchResultsPanel>
            <SearchResultItem />
        </SearchResultsPanel>
    </Search>
);
const MySearch = () => (
    <Search>
        <SearchResultsPanel>
            <SearchResultItem />
        </SearchResultsPanel>
    </Search>
);

<SearchResultItem> renders the content.label and content.description for each result. You can customize what it renders by providing a function as the label and the description props. This function takes the search result as a parameter and must return a React element.

For instance:

import {
    Search,
    SearchResultsPanel,
    SearchResultItem,
} from '@react-admin/ra-search';

const MySearch = () => (
    <Search>
        <SearchResultsPanel>
            <SearchResultItem
                label={record => (
                    <>
                        {record.type === 'artists' ? (
                            <PersonIcon />
                        ) : (
                            <MusicIcon />
                        )}
                        <span>{record.content.label}</span>
                    </>
                )}
            />
        </SearchResultsPanel>
    </Search>
);
import { Search, SearchResultsPanel, SearchResultItem } from "@react-admin/ra-search";

const MySearch = () => (
    <Search>
        <SearchResultsPanel>
            <SearchResultItem
                label={(record) => (
                    <>
                        {record.type === "artists" ? <PersonIcon /> : <MusicIcon />}
                        <span>{record.content.label}</span>
                    </>
                )}
            />
        </SearchResultsPanel>
    </Search>
);

You can also completely replace the search result item component:

import { Search, SearchResultsPanel } from '@react-admin/ra-search';

const MySearchResultItem = ({ data, onClose }) => (
    <li key={data.id} className="highlight">
        <Link to={data.url} onClick={onClose}>
            <strong>{data.content.label}</strong>
        </Link>
        <p>{data.content.description}</p>
    </li>
);

const MySearch = () => (
    <Search>
        <SearchResultsPanel>
            <MySearchResultItem />
        </SearchResultsPanel>
    </Search>
);
import { Search, SearchResultsPanel } from "@react-admin/ra-search";

const MySearchResultItem = ({ data, onClose }) => (
    <li key={data.id} className="highlight">
        <Link to={data.url} onClick={onClose}>
            <strong>{data.content.label}</strong>
        </Link>
        <p>{data.content.description}</p>
    </li>
);

const MySearch = () => (
    <Search>
        <SearchResultsPanel>
            <MySearchResultItem />
        </SearchResultsPanel>
    </Search>
);

Tip: You can customize the highlight of the search terms by overriding the <SearchResultsPanel sx> prop as following:

const CustomSearch = () => (
    <Search>
        <SearchResultsPanel
            sx={{
                '& ::highlight(search)': {
                    backgroundColor: '#7de5fa',
                },
            }}
        />
    </Search>
);

<SearchWithResult>

The <SearchWithResult> component renders a search input and the search results directly below the input. It's ideal for dashboards or menu panels.

By default, <SearchWithResult> will group the search results by target, and show their content.label and content.description.

Usage

Here's how to include the <SearchWithResult> component inside a custom <Dashboard> component:

import { Card, CardContent } from '@mui/material';
import { Admin } from 'react-admin';
import { SearchWithResult } from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';

const MyDashboard = () => (
    <Card>
        <CardContent>
            <SearchWithResult />
        </CardContent>
    </Card>
);

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);
import { Card, CardContent } from "@mui/material";
import { Admin } from "react-admin";
import { SearchWithResult } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";

const MyDashboard = () => (
    <Card>
        <CardContent>
            <SearchWithResult />
        </CardContent>
    </Card>
);

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);

Props

Prop Required Type Default Description
children Optional Element <SearchResultsPanel> A component that will display the results.
disableHighlight Optional boolean false Disable the highlight of the search term of each result.
onNavigate Optional function () => undefined A callback function to run when the user navigate to a result.
options Optional Object - An object containing options to apply to the search.
queryOptions Optional UseQuery Options - react-query options for the search query
wait Optional number 500 The delay of debounce for the search to launch after typing in ms.

children

The <SearchWithResult> children allow you to customize the way results are displayed. The child component can grab the search result using the useSearchResult hook.

import { Admin } from 'react-admin';
import { SearchWithResult, useSearchResults } from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';

const MyDashboard = () => (
    <SearchWithResult>
        <MySearchResultsPanel />
    </SearchWithResult>
);

const MySearchResultsPanel = () => {
    const { data } = useSearchResults();
    return (
        <ul>
            {data.map(item => (
                <li key={item.id}>{item.content.label}</li>
            ))}
        </ul>
    );
};

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);
import { Admin } from "react-admin";
import { SearchWithResult, useSearchResults } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";

const MyDashboard = () => (
    <SearchWithResult>
        <MySearchResultsPanel />
    </SearchWithResult>
);

const MySearchResultsPanel = () => {
    const { data } = useSearchResults();
    return (
        <ul>
            {data.map((item) => (
                <li key={item.id}>{item.content.label}</li>
            ))}
        </ul>
    );
};

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);

disableHighlight

The search terms in each result are highlighted. You can disable this feature with the disableHighlight prop as follows:

<SearchWithResults disableHighlight />
<SearchWithResults disableHighlight />;

Tip: To customize the highlight style check out the Customizing the result items section below.

onNavigate

onNavigate allows you to perform an action when the user clicks on a search result, e.g. to close a menu (See below for an example with <SolarLayout>).

import { Admin } from 'react-admin';
import { SearchWithResult } from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';

const MyDashboard = () => {
    const handleNavigate = () => {
        console.log('User navigated to a result');
    };
    return <SearchWithResult onNavigate={handleNavigate} />;
};

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);
import { Admin } from "react-admin";
import { SearchWithResult } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";

const MyDashboard = () => {
    const handleNavigate = () => {
        console.log("User navigated to a result");
    };
    return <SearchWithResult onNavigate={handleNavigate} />;
};

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);

options

An object containing options to apply to the search:

  • targets: string[]: an array of the indices on which to perform the search. Defaults to an empty array.
  • {any}: {any}: any custom option to pass to the search engine.
<SearchWithResult options={{ foo: 'bar' }} />
<SearchWithResult options={{ foo: "bar" }} />;

queryOptions

<SearchWithResult> accepts a queryOptions prop to pass options to the react-query client. This can be useful e.g. to override the default side effects such as onSuccess or onError.

<SearchWithResult queryOptions={{ onSuccess: data => console.log(data) }} />
<SearchWithResult queryOptions={{ onSuccess: (data) => console.log(data) }} />;

wait

The number of milliseconds to wait before processing the search request, immediately after the user enters their last character.

<SearchWithResult wait={200} />
<SearchWithResult wait={200} />;

Customizing the Entire Search Results

Pass a custom React element as a child of <SearchWithResult> to customize the appearance of the search results. This can be useful e.g. to customize the results grouping, or to arrange search results differently.

ra-search renders the <SearchResultsPanel> inside a SearchContext. You can use the useSearchResult hook to read the search results, as follows:

import { Card, CardContent } from '@mui/material';
import { Link } from 'react-router-dom';
import { Admin } from 'react-admin';
import {
    SearchWithResult,
    SearchResultsPanel,
    useSearchResults,
} from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';

const MyDashboard = () => (
    <Card>
        <CardContent>
            <SearchWithResult>
                <MySearchResultsPanel />
            </SearchWithResult>
        </CardContent>
    </Card>
);

const MySearchResultsPanel = () => {
    const { data } = useSearchResults();
    return (
        <ul style={{ maxHeight: '250px', overflow: 'auto' }}>
            {data.map(item => (
                <li key={item.id}>
                    <Link to={item.url}>
                        <strong>{item.content.label}</strong>
                    </Link>
                    <p>{item.content.description}</p>
                </li>
            ))}
        </ul>
    );
};

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);
import { Card, CardContent } from "@mui/material";
import { Link } from "react-router-dom";
import { Admin } from "react-admin";
import { SearchWithResult, useSearchResults } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";

const MyDashboard = () => (
    <Card>
        <CardContent>
            <SearchWithResult>
                <MySearchResultsPanel />
            </SearchWithResult>
        </CardContent>
    </Card>
);

const MySearchResultsPanel = () => {
    const { data } = useSearchResults();
    return (
        <ul style={{ maxHeight: "250px", overflow: "auto" }}>
            {data.map((item) => (
                <li key={item.id}>
                    <Link to={item.url}>
                        <strong>{item.content.label}</strong>
                    </Link>
                    <p>{item.content.description}</p>
                </li>
            ))}
        </ul>
    );
};

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);

Customizing The Result Items

By default, <SearchWithResult> displays the results in <SearchResultsPanel>, which displays each results in a <SearchResultItem>. So rendering <SearchWithResult> without children is equivalent to rendering:

const MySearch = () => (
    <SearchWithResult>
        <SearchResultsPanel>
            <SearchResultItem />
        </SearchResultsPanel>
    </SearchWithResult>
);
const MySearch = () => (
    <SearchWithResult>
        <SearchResultsPanel>
            <SearchResultItem />
        </SearchResultsPanel>
    </SearchWithResult>
);

<SearchResultItem> renders the content.label and content.description for each result. You can customize what it renders by providing a function as the label and the description props. This function takes the search result as a parameter and must return a React element.

For instance:

import { Card, CardContent } from '@mui/material';
import Groups3Icon from '@mui/icons-material/Groups3';
import LibraryMusicIcon from '@mui/icons-material/LibraryMusic';
import { Admin } from 'react-admin';
import {
    SearchWithResult,
    SearchResultsPanel,
    SearchResultItem,
    useSearchResults,
} from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';

const MyDashboard = () => (
    <Card>
        <CardContent>
            <SearchWithResult>
                <SearchResultsPanel>
                    <SearchResultItem
                        label={record => (
                            <>
                                {record.type === 'artists' ? (
                                    <Groups3Icon />
                                ) : (
                                    <LibraryMusicIcon />
                                )}
                                <span>{record.content.label}</span>
                            </>
                        )}
                    />
                </SearchResultsPanel>
            </SearchWithResult>
        </CardContent>
    </Card>
);

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);
import { Card, CardContent } from "@mui/material";
import Groups3Icon from "@mui/icons-material/Groups3";
import LibraryMusicIcon from "@mui/icons-material/LibraryMusic";
import { Admin } from "react-admin";
import { SearchWithResult, SearchResultsPanel, SearchResultItem } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";

const MyDashboard = () => (
    <Card>
        <CardContent>
            <SearchWithResult>
                <SearchResultsPanel>
                    <SearchResultItem
                        label={(record) => (
                            <>
                                {record.type === "artists" ? <Groups3Icon /> : <LibraryMusicIcon />}
                                <span>{record.content.label}</span>
                            </>
                        )}
                    />
                </SearchResultsPanel>
            </SearchWithResult>
        </CardContent>
    </Card>
);

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);

You can also completely replace the search result item component:

import { Card, CardContent } from '@mui/material';
import { Link } from 'react-router-dom';
import { Admin } from 'react-admin';
import {
    SearchWithResult,
    SearchResultsPanel,
    SearchResultItem,
} from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';

const MySearchResultItem = ({ data }) => (
    <li key={data.id} className="highlight">
        <Link to={data.url}>
            <strong>{data.content.label}</strong>
        </Link>
        <p>{data.content.description}</p>
    </li>
);

const MyDashboard = () => (
    <Card>
        <CardContent>
            <SearchWithResult>
                <SearchResultsPanel>
                    <MySearchResultItem />
                </SearchResultsPanel>
            </SearchWithResult>
        </CardContent>
    </Card>
);

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);
import { Card, CardContent } from "@mui/material";
import { Link } from "react-router-dom";
import { Admin } from "react-admin";
import { SearchWithResult, SearchResultsPanel } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";

const MySearchResultItem = ({ data }) => (
    <li key={data.id} className="highlight">
        <Link to={data.url}>
            <strong>{data.content.label}</strong>
        </Link>
        <p>{data.content.description}</p>
    </li>
);

const MyDashboard = () => (
    <Card>
        <CardContent>
            <SearchWithResult>
                <SearchResultsPanel>
                    <MySearchResultItem />
                </SearchResultsPanel>
            </SearchWithResult>
        </CardContent>
    </Card>
);

export const App = () => (
    <Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
        {/*...*/}
    </Admin>
);

Tip: You can customize the highlight of the search terms by overriding the <SearchResultsPanel sx> prop as following:

const CustomSearch = () => (
    <SearchWithResult>
        <SearchResultsPanel
            sx={{
                '& ::highlight(search)': {
                    backgroundColor: '#7de5fa',
                },
            }}
        />
    </SearchWithResult>
);

Use It With SolarLayout

The <SearchWithResult> component works perfectly when used inside the <SolarLayout> menu.

The useSolarSidebarActiveMenu hook combined with the onNavigate prop allow you to close the <SolarMenu> when the user selects an element in the result.

Here is an implementation example:

import { Admin } from 'react-admin';
import { Box } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AlbumIcon from '@mui/icons-material/Album';
import Groups3Icon from '@mui/icons-material/Groups3';
import {
    SolarLayout,
    SolarLayoutProps,
    SolarMenu,
    useSolarSidebarActiveMenu,
} from '@react-admin/ra-navigation';
import { SearchWithResult } from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';

const MySolarLayout = (props: SolarLayoutProps) => (
    <SolarLayout {...props} menu={MySolarMenu} />
);

const MySolarMenu = () => (
    <SolarMenu bottomToolbar={<CustomBottomToolbar />}>
        <SolarMenu.Item
            name="artists"
            to="/artists"
            icon={<Groups3Icon />}
            label="resources.stores.name"
        />
        <SolarMenu.Item
            name="songs"
            to="/songs"
            icon={<AlbumIcon />}
            label="resources.events.name"
        />
    </SolarMenu>
);

const CustomBottomToolbar = () => (
    <>
        <SearchMenuItem />
        <SolarMenu.LoadingIndicatorItem />
    </>
);

const SearchMenuItem = () => {
    const [, setActiveMenu] = useSolarSidebarActiveMenu();
    const handleClose = () => {
        setActiveMenu('');
    };

    return (
        <SolarMenu.Item
            icon={<SearchIcon />}
            label="Search"
            name="search"
            subMenu={
                <Box sx={{ maxWidth: 298 }}>
                    <SearchWithResult onNavigate={handleClose} />
                </Box>
            }
            data-testid="search-button"
        />
    );
};

export const App = () => (
    <Admin dataProvider={searchDataProvider} layout={MySolarLayout}>
        {/*...*/}
    </Admin>
);
import { Admin } from "react-admin";
import { Box } from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import AlbumIcon from "@mui/icons-material/Album";
import Groups3Icon from "@mui/icons-material/Groups3";
import { SolarLayout, SolarMenu, useSolarSidebarActiveMenu } from "@react-admin/ra-navigation";
import { SearchWithResult } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";

const MySolarLayout = (props) => <SolarLayout {...props} menu={MySolarMenu} />;

const MySolarMenu = () => (
    <SolarMenu bottomToolbar={<CustomBottomToolbar />}>
        <SolarMenu.Item name="artists" to="/artists" icon={<Groups3Icon />} label="resources.stores.name" />
        <SolarMenu.Item name="songs" to="/songs" icon={<AlbumIcon />} label="resources.events.name" />
    </SolarMenu>
);

const CustomBottomToolbar = () => (
    <>
        <SearchMenuItem />
        <SolarMenu.LoadingIndicatorItem />
    </>
);

const SearchMenuItem = () => {
    const [, setActiveMenu] = useSolarSidebarActiveMenu();
    const handleClose = () => {
        setActiveMenu("");
    };

    return (
        <SolarMenu.Item
            icon={<SearchIcon />}
            label="Search"
            name="search"
            subMenu={
                <Box sx={{ maxWidth: 298 }}>
                    <SearchWithResult onNavigate={handleClose} />
                </Box>
            }
            data-testid="search-button"
        />
    );
};

export const App = () => (
    <Admin dataProvider={searchDataProvider} layout={MySolarLayout}>
        {/*...*/}
    </Admin>
);

useSearch

Just like useUpdate, useSearch returns a function allowing to call the dataProvider.search(), as well as a state object for the response. Use it to create your own <Search> component.

import { useState } from 'React';
import { TextField } from '@mui/material';
import {
    useSearch,
    SearchResultContextProvider,
    SearchResultsPanel,
} from '@react-admin/ra-search';

const Search = () => {
    const [query, setQuery] = useState<string | undefined>();
    const [open, setOpen] = useState(false);
    const [search, searchResultState] = useSearch();

    const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
        setQuery(event.target.value);
        search(event.target.value);
        setOpen(true);
    };

    const handleClose = () => {
        setOpen(false);
    };

    const contextValue = useMemo(() => ({
        onClose: handleClose,
        ...searchResultState,
    }));

    return (
        <>
            <TextField value={query} onChange={handleChange} />
            <Modal
                open={open}
                onClose={handleClose}
                aria-labelledby="modal-modal-title"
            >
                <Typography id="modal-modal-title" variant="h6" component="h2">
                    Search results for: {searchResultState.query}
                </Typography>
                <SearchResultContextProvider value={contextValue}>
                    <SearchResultsPanel />
                </SearchResultContextProvider>
            </Modal>
        </>
    );
};
import { useState } from "React";
import { TextField } from "@mui/material";
import { useSearch, SearchResultContextProvider, SearchResultsPanel } from "@react-admin/ra-search";

const Search = () => {
    const [query, setQuery] = useState();
    const [open, setOpen] = useState(false);
    const [search, searchResultState] = useSearch();

    const handleChange = (event) => {
        setQuery(event.target.value);
        search(event.target.value);
        setOpen(true);
    };

    const handleClose = () => {
        setOpen(false);
    };

    const contextValue = useMemo(() => ({
        onClose: handleClose,
        ...searchResultState,
    }));

    return (
        <>
            <TextField value={query} onChange={handleChange} />
            <Modal open={open} onClose={handleClose} aria-labelledby="modal-modal-title">
                <Typography id="modal-modal-title" variant="h6" component="h2">
                    Search results for: {searchResultState.query}
                </Typography>
                <SearchResultContextProvider value={contextValue}>
                    <SearchResultsPanel />
                </SearchResultContextProvider>
            </Modal>
        </>
    );
};

By default, the <Search> component allows the user to navigate search results with the arrow keys. If you want to reimplement it, you have to use the useArrowKeysToNavigate hook.

Pass the list ref to the hook; also, each result must have a button role:

import { List, ListItem } from '@mui/material';

const SearchResults = () => {
    const listRef = React.useRef<HTMLUListElement>(null);
    useArrowKeysToNavigate(listRef);

    <List ref={listRef}>
        {data.map(resultData => {
            return <ListItem button data={resultData} key={resultData.id} />;
        })}
    </List>;
};
import { List, ListItem } from "@mui/material";

const SearchResults = () => {
    const listRef = React.useRef(null);
    useArrowKeysToNavigate(listRef);

    <List ref={listRef}>
        {data.map((resultData) => {
            return <ListItem button data={resultData} key={resultData.id} />;
        })}
    </List>;
};

I18N

You can customize the <Search> component text, by importing the custom messages into your i18nProvider:

// in src/i18nProvider.ts
import { mergeTranslations } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import {
    raSearchEnglishMessages,
    raSearchFrenchMessages,
} from '@react-admin/ra-search';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';

export const i18nProvider = polyglotI18nProvider(locale => {
    if (locale === 'fr') {
        return mergeTranslations(frenchMessages, raSearchFrenchMessages);
    }
    // Always fallback on english
    return mergeTranslations(englishMessages, raSearchEnglishMessages);
}, 'en');
// in src/i18nProvider.ts
import { mergeTranslations } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import { raSearchEnglishMessages, raSearchFrenchMessages } from "@react-admin/ra-search";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";

export const i18nProvider = polyglotI18nProvider((locale) => {
    if (locale === "fr") {
        return mergeTranslations(frenchMessages, raSearchFrenchMessages);
    }
    // Always fallback on english
    return mergeTranslations(englishMessages, raSearchEnglishMessages);
}, "en");

CHANGELOG

v5.2.0

2024-10-01

  • (feat) Add ability to start a search with a keyboard shortcut (Ctrl/Cmd+K)

v5.1.2

2024-08-19

  • (fix) Bump highlight-search-term to 1.0.3 to fix the "Unexpected Token Export" error obtained when using the package as CJS

v5.1.1

2024-08-09

  • (fix) Fix dependency to highlight-search-term.

v5.1.0

2024-08-06

  • (feat) Highlight search term in <Search> and <SearchWithResults> results

v5.0.0

2024-07-25

  • Upgrade to react-admin v5
  • The state returned by the useSearch() hook additionally returns all useQuery state
  • Fix inflection imports

Breaking Change

  • The useSearch() hook takes an object as argument instead of a two arguments:
-    const [search, searchData] = useSearch(
-        {
-            targets: ['artists', 'songs'],
-        },
-        {
-            meta: { foo: 'bar' },
-        }
-    );
+    const [search, searchData] = useSearch({
+        options: {
+            targets: ['artists', 'songs'],
+        },
+        queryOptions: {
+            meta: { foo: 'bar' },
+        },
+    });
  • The QueryOptions interface of the useSearch() hook was renammed to UseSearchOptions
  • Remove PropTypes
  • the data prop returned by the useSearchResults hook might be undefined. To avoid TypeScript complaints, you should check if data is undefined before using it:
 const MySearchResult = (): ReactElement => {
     const { data, onClose } = useSearchResults();

     return (
         <ul>
-            {data.map(item => (
+            {data?.map(item => (
                 <li key={item.id}>
                     <Link to={item.url} onClick={onClose}>
                         {item.content.label}
                     </Link>
                     <p>{item.content.description}</p>
                 </li>
             ))}
         </ul>
     );
 };

## v4.6.0

> 2024-04-05

-   (feat) Add queryOptions prop to `<Search>` and `<SearchWithResult>`

## v4.5.3

> 2024-02-29

-   (fix) Fix minimum required version of `@mui/material` and `@mui/icons-material`

## v4.5.2

> 2024-02-08

-   (fix) Fix `<Search>` ignores the `historySize` and `options` props
-   (fix) Fix `<SearchWithResult>` ignores the `options` prop

## v4.5.1

> 2023-12-01

-   (fix) Fix `<Search>` prop types don't allow to customize `<TextField>` props

## v4.5.0

> 2023-11-10

-   (fix) `<SearchInput>`, `<Search>` and `<SearchWithResult>` don't use MUI theme effectively.
-   (feat) Introduce a new prop `isInAppBar` to `<Search>` component in order to use specific style for admin `appBar`.

**Breaking Change**

-   `color` prop support is removed to delegate color and background color of components to MUI theme.

## v4.4.0

> 2023-10-15

-   (feat) Add `<SearchWithResult>`, a search component for dashboards or menu panel, where the results appear below the search input rather than in a popover.
-   (feat) Display total number of results per group in `<SearchResultsGroup>`'s subheader

## v4.3.0

> 2023-05-24

-   Upgraded to react-admin `4.10.6`

## v4.2.0

> 2022-12-22

-   (feat) Move search history from `useSearch` hook to `<Search>` component
-   (fix) Fix history shows too many items
-   (fix) Fix UI glitches in `<SearchInput>`
-   (fix) Fix `<Search>` component requires an i18n provider to be set

## v4.1.2

> 2022-11-30

-   (feat) Use `react-query` to manage `useSearch` hook queries

## v4.1.1

> 2022-10-20

-   (feat) Export `SearchHistoryPanel` and `SearchHistoryItem` components.

## v4.1.0

> 2022-08-19

-   (feat) Add `redirect` option to `addSearchMethod`, offering ability to choose the page to redirect to (edit or show)

## v4.0.5

> 2022-08-18

-   (doc) Fix documentation still mentioning `useSearchResultContext` instead of `useSearchResults`

## v4.0.4

> 2022-08-09

-   (fix) Backport: Fix `onSelect` parameter types

## v4.0.3

> 2022-06-28

-   (doc) Fix documentation referencing non existent hooks
-   (doc) Fix documentation incorrect instructions to setup of the `SearchContext`

## v4.0.2

> 2022-06-14

-   (fix) Use theme `shape.borderRadius` settings for hover effect

## v4.0.1

> 2022-06-08

-   (fix) Update peer dependencies ranges (support React 18)

## v4.0.0

> 2022-06-07

-   Upgrade to react-admin v4

**Breaking Change**

-   The state returned by `useSearch()` now uses `isLoading` instead of `loading`

```diff
-const [doSearch, { loading }] = useSearch('foo');
+const [doSearch, { isLoading }] = useSearch('foo');

v2.2.1

2021-06-29

  • (fix) Update peer dependencies ranges (support react 17)

v2.2.0

2021-04-29

  • (feat) Add a search history

v2.1.0

2021-04-22

  • (feat) Allows users to navigate in results with up and down arrow keys

v2.0.1

2020-12-18

  • (fix) Fix search results panel markup and accessibility

v2.0.0

2020-12-17

  • (feat) rename search method options (facets -> targets)

v1.2.3

2020-12-08

  • (fix) Fix SearchInput value is not selectable while search results are displayed

v1.2.2

2020-12-03

  • (fix) Fix part of terms are sometimes discarded when response is slow
  • (fix) Fix empty popover is displayed when there are no more results

v1.2.1

2020-11-23

  • (fix) Fix search results popover closes and reopens at each keystroke

v1.2.0

2020-11-04

  • (feat) Export groupSearchResultsByResource function

v1.1.1

2020-10-19

  • (fix) Display the <PopOver> position at the bottom right of the <SearchInput>
  • (fix) Remove the stickyness of facet headers
  • (feat) Add a growing effect when focusing the menu

v1.1.0

2020-10-14

  • (fix) Hide clear button when there is no value in the <SearchInput>
  • (feat) Add color prop to both <SearchInput> and <Search> component supporting light and dark

v1.0.0

2020-10-13

  • First release