react-admin ≥ 4.14.0

ra-search

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.
historySize Optional number 5 The number of past queries to keep in history.
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.
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>
);

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} />;

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" }} />;

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}>
    <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}>
    <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>
);

<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.
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.
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>
);

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" }} />;

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}>
    <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}>
    <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>
);

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

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
-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