ra-preferences

Persist user preferences (language, theme, filters, datagrid columns, sidebar position, etc) in local storage.

ra-preferences

These preferences are device dependent, so this module is particularly fitted for UI preferences. If a user has several instances of the admin opened in several tabs, changes in the preferences in one tab trigger an update in the other tabs. Note that if the user browses in incognito mode, the preferences won't be saved.

It also allows users to configure parts of the UI to better suits their needs.

ra-preferences-no-code

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

Installation

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

Tip: ra-preferences 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.

In order to use the components and hooks allowing users to configure their admin (no-code), you must include the <PreferencesEditorContextProvider> in your layout and a button in your <AppBar> to open the editor sidebar:

// in src/MyAppbar.js
import { AppBar, AppBarProps } from "react-admin";
import { Typography } from "@material-ui/core";
import { PreferencesEditorButton } from "@react-admin/ra-no-code";

export const MyAppbar = (props: AppBarProps) => (
  <AppBar {...props}>
    <Typography variant="h6" color="inherit" id="react-admin-title" />
    <span style={{ flex: 1 }} />
    // This button is required for users to open the preferences editor sidebar
    <PreferencesEditorButton />
  </AppBar>
);

// in src/MyLayout.js
import { Layout, LayoutProps } from "react-admin";
import { WithInspector } from "@react-admin/ra-no-code";
import { MyAppbar } from "./MyAppbar";

export const MyLayout = (props: LayoutProps) => (
  <WithInspector {...props}>
    <Layout appBar={MyAppbar} {...props} />
  </WithInspector>
);

// in src/App.js
import { MyLayout } from "./MyLayout";

const Application = () => (
  <Admin
    layout={MyLayout}
    // ...
  >
    // ...
  </Admin>
);
// in src/MyAppbar.js
import { AppBar } from "react-admin";
import { Typography } from "@material-ui/core";
import { PreferencesEditorButton } from "@react-admin/ra-no-code";

export const MyAppbar = (props) => (
  <AppBar {...props}>
    <Typography variant="h6" color="inherit" id="react-admin-title" />
    <span style={{ flex: 1 }} />
    // This button is required for users to open the preferences editor sidebar
    <PreferencesEditorButton />
  </AppBar>
);

// in src/MyLayout.js
import { Layout } from "react-admin";
import { WithInspector } from "@react-admin/ra-no-code";
import { MyAppbar } from "./MyAppbar";

export const MyLayout = (props) => (
  <WithInspector {...props}>
    <Layout appBar={MyAppbar} {...props} />
  </WithInspector>
);

// in src/App.js
import { MyLayout } from "./MyLayout";

const Application = () => <Admin layout={MyLayout}>// ...</Admin>;

usePreferences: Reading and Writing User Preferences

The usePreferences hook behaves like setState. It returns a value and a setter for the value, in an array. Depending on the argument passed to usePreferences, the return tuple concerns either a single value, or the whole preference tree.

Here is how to read a single value from the preference store, with a default value:

import { usePreferences } from "@react-admin/ra-preferences";

const PostList = (props) => {
  const [density] = usePreferences("posts.list.density", "small");

  return (
    <List {...props}>
      <Datagrid size={density}>...</Datagrid>
    </List>
  );
};
import { usePreferences } from "@react-admin/ra-preferences";

const PostList = (props) => {
  const [density] = usePreferences("posts.list.density", "small");

  return (
    <List {...props}>
      <Datagrid size={density}>...</Datagrid>
    </List>
  );
};

To write a single value use the second return value:

const ChangeDensity = () => {
  const [density, setDensity] = usePreferences("posts.list.density", "small");

  const changeDensity = (): void => {
    setDensity(density === "small" ? "medium" : "small");
  };

  return (
    <Button onClick={changeDensity}>
      {`Change density (current ${density})`}
    </Button>
  );
};
const ChangeDensity = () => {
  const [density, setDensity] = usePreferences("posts.list.density", "small");

  const changeDensity = () => {
    setDensity(density === "small" ? "medium" : "small");
  };

  return (
    <Button onClick={changeDensity}>
      {`Change density (current ${density})`}
    </Button>
  );
};

To read and write the entire preferences tree, don't pass any argument to the hook. You will find this option useful when building a preferences Form:

import { usePreferences } from "@react-admin/ra-preferences";
import { useNotify } from "react-admin";
import { Form, Field } from "react-final-form";

const PreferencesPane = () => {
  const [preferences, setPreferences] = usePreferences();
  const notify = useNotify();

  const handleSave = (values) => {
    setPreferences(values);
    notify("preferences saved");
  };

  return (
    <Form
      initialValues={preferences}
      onSubmit={handleSave}
      render={({ handleSubmit }) => (
        <form onSubmit={handleSubmit}>
          <div>
            <label>Post list density</label>
            <Field name="posts.list.density" component="select">
              <option value="small">Small</option>
              <option value="medium">Medium</option>
            </Field>
          </div>
          <button type="submit">Submit</button>
        </form>
      )}
    />
  );
};
import { usePreferences } from "@react-admin/ra-preferences";
import { useNotify } from "react-admin";
import { Form, Field } from "react-final-form";

const PreferencesPane = () => {
  const [preferences, setPreferences] = usePreferences();
  const notify = useNotify();

  const handleSave = (values) => {
    setPreferences(values);
    notify("preferences saved");
  };

  return (
    <Form
      initialValues={preferences}
      onSubmit={handleSave}
      render={({ handleSubmit }) => (
        <form onSubmit={handleSubmit}>
          <div>
            <label>Post list density</label>
            <Field name="posts.list.density" component="select">
              <option value="small">Small</option>
              <option value="medium">Medium</option>
            </Field>
          </div>
          <button type="submit">Submit</button>
        </form>
      )}
    />
  );
};

Tip: The preferences API is synchronous, because preferences are stored in memory, and replicated in localStorage. So even though localStorage has an async API, the preferences API is synchronous.

<PreferencesSetter>: Setting Preferences Declaratively

A special component called <PreferencesSetter> lets you define application preferences by using it anywhere in our component tree.

To use it, just wrap any component that need to use the corresponding preference with <PreferencesSetter path="my.preference" value="myvalue">. This wrapping needs to be done to ensure that the corresponding preference is set before rendering the wrapped component.

<PreferencesSetter path="list.density" value="small">
  <MyPreferencesDependentComponent />
</PreferencesSetter>;
<PreferencesSetter path="list.density" value="small">
  <MyPreferencesDependentComponent />
</PreferencesSetter>;

Using <PreferencesSetter> is equivalent to using usePreferences and setting its value directly.

const [_, setDensity] = usePreferences("list.density");

useEffect(() => {
  setDensity("small");
}, []);
const [_, setDensity] = usePreferences("list.density");

useEffect(() => {
  setDensity("small");
}, []);

Tip: <PreferencesSetter> is a good candidate to make your life easier when writing unit tests. When it comes to mock preferences, use it rather than mock localstorage values.

Tip: The use of this component has a direct impact on the writings in the localstorage. It is advised to use it sparingly to avoid bottlenecks.

<ToggleThemeButton>: Store the Theme in the Preferences

Many admin UIs offer a dark theme, and the user expect their choice of theme to be persistent across sessions. ra-preferences offer two components to facilitate the implementation of that feature: <PreferencesBasedThemeProvider>, and <ToggleThemeButton>.

First, wrap your <Admin> in a <PreferencesBasedThemeProvider> to allow the modification of the theme from inside the application:

import React from "react";
import { Admin, Resource } from "react-admin";
import { PreferencesBasedThemeProvider } from "@react-admin/ra-preferences";

export const ThemeInPreferences = () => (
  <PreferencesBasedThemeProvider>
    <Admin dataProvider={dataProvider} layout={MyLayout}>
      <Resource name="posts" list={PostList} />
    </Admin>
  </PreferencesBasedThemeProvider>
);
import React from "react";
import { Admin, Resource } from "react-admin";
import { PreferencesBasedThemeProvider } from "@react-admin/ra-preferences";

export const ThemeInPreferences = () => (
  <PreferencesBasedThemeProvider>
    <Admin dataProvider={dataProvider} layout={MyLayout}>
      <Resource name="posts" list={PostList} />
    </Admin>
  </PreferencesBasedThemeProvider>
);

Next, insert the <ToggleThemeButton> in the UI, e.g. in the top app bar:

import React from "react";
import { Layout, LayoutProps, AppBar, AppBarProps } from "react-admin";
import { Box, Typography } from "@material-ui/core";
import { ToggleThemeButton } from "@react-admin/ra-preferences";

const MyAppBar = (props: AppBarProps) => (
  <AppBar {...props}>
    <Box flex="1">
      <Typography variant="h6" id="react-admin-title"></Typography>
    </Box>
    <ToggleThemeButton />
  </AppBar>
);

const MyLayout = (props: LayoutProps) => (
  <Layout {...props} appBar={MyAppBar} />
);
import React from "react";
import { Layout, AppBar } from "react-admin";
import { Box, Typography } from "@material-ui/core";
import { ToggleThemeButton } from "@react-admin/ra-preferences";

const MyAppBar = (props) => (
  <AppBar {...props}>
    <Box flex="1">
      <Typography variant="h6" id="react-admin-title"></Typography>
    </Box>
    <ToggleThemeButton />
  </AppBar>
);

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

Now the user can switch between light and dark theme, and their choice will be shared across tabs, and remembered during future sessions.

<LanguageSwitcher>: Store the Locale in Preferences

In multilingual applications, users can select the locale using a language switcher. They expect that choice to be persistent across sections, so binding usePreferences with a language section is a common need.

To address that need, ra-preferences proposes a <LanguageSwitcher> component that manages the language change and persistence altogether:

import React from "react";
import { LanguageSwitcher } from "@react-admin/ra-preferences";
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";
import {
  Admin,
  Resource,
  List,
  SimpleList,
  Layout,
  LayoutProps,
  AppBar,
  AppBarProps,
} from "react-admin";
import { Box, Typography } from "@material-ui/core";

const MyAppBar = (props: AppBarProps) => (
  <AppBar {...props}>
    <Box flex="1">
      <Typography variant="h6" id="react-admin-title"></Typography>
    </Box>
    <LanguageSwitcher
      languages={[
        { locale: "en", name: "English" },
        { locale: "fr", name: "Français" },
      ]}
    />
  </AppBar>
);

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

const i18nProvider = polyglotI18nProvider(
  (locale) => (locale === "fr" ? frenchMessages : englishMessages),
  "en" // Default locale
);

const App = () => (
  <Admin
    i18nProvider={i18nProvider}
    dataProvider={dataProvider}
    layout={MyLayout}
  >
    <Resource name="posts" list={PostList} />
  </Admin>
);
import React from "react";
import { LanguageSwitcher } from "@react-admin/ra-preferences";
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";
import { Admin, Resource, Layout, AppBar } from "react-admin";
import { Box, Typography } from "@material-ui/core";

const MyAppBar = (props) => (
  <AppBar {...props}>
    <Box flex="1">
      <Typography variant="h6" id="react-admin-title"></Typography>
    </Box>
    <LanguageSwitcher
      languages={[
        { locale: "en", name: "English" },
        { locale: "fr", name: "Français" },
      ]}
    />
  </AppBar>
);

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

const i18nProvider = polyglotI18nProvider(
  (locale) => (locale === "fr" ? frenchMessages : englishMessages),
  "en" // Default locale
);

const App = () => (
  <Admin
    i18nProvider={i18nProvider}
    dataProvider={dataProvider}
    layout={MyLayout}
  >
    <Resource name="posts" list={PostList} />
  </Admin>
);

If you want the persistent locale change functionality but not the UI, you can use the useSetLocaleAndPreference hook instead, which works just like react-admin's setLocale hook:

import { useSetLocaleAndPreference } from "@react-admin/ra-preferences";

const availableLanguages = {
  en: "English",
  fr: "Français",
};
const LanguageSwitcher = () => {
  const setLocale = useSetLocaleAndPreference();
  return (
    <ul>
      {Object.keys(availableLanguages).map((locale) => {
        <li key={locale} onClick={() => setLocale(locale)}>
          {availableLanguages[locale]}
        </li>;
      })}
    </ul>
  );
};
import { useSetLocaleAndPreference } from "@react-admin/ra-preferences";

const availableLanguages = {
  en: "English",
  fr: "Français",
};
const LanguageSwitcher = () => {
  const setLocale = useSetLocaleAndPreference();
  return (
    <ul>
      {Object.keys(availableLanguages).map((locale) => {
        <li key={locale} onClick={() => setLocale(locale)}>
          {availableLanguages[locale]}
        </li>;
      })}
    </ul>
  );
};

<SidebarOpenPreferenceSync>: Store the Sidebar Open/Close State in Preferences

Some users prefer the sidebar opened, other prefer it closed. Those who close the sidebar once usually don't like to have to close it again when they reload the app.

The <SidebarOpenPreferenceSync> component saves the sidebar visibility in Preferences, and restores it on load. Users only have to hide the sidebar once per browser, and the sidebar will be closed even for future sessions.

Use this component inside a react-admin app, for instance in a custom <Layout>:

import { Admin, Layout, LayoutProps } from "react-admin";

import { SidebarOpenPreferenceSync } from "@react-admin/ra-preferences";

const MyLayout = (props: LayoutProps) => (
  <>
    <SidebarOpenPreferenceSync />
    <Layout {...props} />
  </>
);

export const App = () => (
  <Admin dataProvider={dataProvider} layout={MyLayout}>
    ...
  </Admin>
);
import { Admin, Layout } from "react-admin";

import { SidebarOpenPreferenceSync } from "@react-admin/ra-preferences";

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

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

<SelectColumnsButton>: Store Datagrid Columns in Preferences

Some admins expose datagrids with many columns, and let users choose which columns they want to show/hide. This setting should be stored in preferences, and restored when the application opens again. For this purpose, ra-preferences offers a component, <SelectColumnsButton>, and a hook, useSelectedColumns().

They both rely on the same two settings:

  • preference: The preference key where the colums selection is stored, e.g. 'posts.list.columns'
  • columns An object listing the column elements, e.g. { id: <TextField source="id" />, title: <TextField source="title" /> }. These columns element will be later passed as children of <Datagrid>.

In addition, useSelectedColumns() accepts a third optional setting called omit. It should contain an array of column names to omit by default (e.g. ['nb_views', 'published']).

Here is an example implementation:

import {
  TopToolbar,
  List,
  ListProps,
  Datagrid,
  TextField,
  NumberField,
  DateField,
} from "react-admin";
import {
  SelectColumnsButton,
  useSelectedColumns,
} from "@react-admin/ra-preferences";

/**
 * The columns list must an object where the key is the column name,
 * and the value a React Element (usually a <Field> element).
 */
const postListColumns = {
  title: <TextField source="title" />,
  teaser: <TextField source="artist" />,
  body: <TextField source="writer" />,
  author: <TextField source="producer" />,
  nb_views: <NumberField source="rank" />,
  published: <DateField source="released" />,
};

// add the <SelectColumnsButton> to the toolbar
const PostActions = () => (
  <TopToolbar>
    <SelectColumnsButton
      preference="posts.list.columns"
      columns={postListColumns}
    />
  </TopToolbar>
);

// get Datagrid children using useSelectedColumns()
const PostList = (props: ListProps) => {
  const columns = useSelectedColumns({
    preferences: "posts.list.columns",
    columns: postListColumns,
    omit: ["nb_views"],
  });
  return (
    <List actions={<PostActions />} {...props}>
      <Datagrid rowClick="edit">{columns}</Datagrid>
    </List>
  );
};
import {
  TopToolbar,
  List,
  Datagrid,
  TextField,
  NumberField,
  DateField,
} from "react-admin";
import {
  SelectColumnsButton,
  useSelectedColumns,
} from "@react-admin/ra-preferences";

/**
 * The columns list must an object where the key is the column name,
 * and the value a React Element (usually a <Field> element).
 */
const postListColumns = {
  title: <TextField source="title" />,
  teaser: <TextField source="artist" />,
  body: <TextField source="writer" />,
  author: <TextField source="producer" />,
  nb_views: <NumberField source="rank" />,
  published: <DateField source="released" />,
};

// add the <SelectColumnsButton> to the toolbar
const PostActions = () => (
  <TopToolbar>
    <SelectColumnsButton
      preference="posts.list.columns"
      columns={postListColumns}
    />
  </TopToolbar>
);

// get Datagrid children using useSelectedColumns()
const PostList = (props) => {
  const columns = useSelectedColumns({
    preferences: "posts.list.columns",
    columns: postListColumns,
    omit: ["nb_views"],
  });
  return (
    <List actions={<PostActions />} {...props}>
      <Datagrid rowClick="edit">{columns}</Datagrid>
    </List>
  );
};

<SavedQueriesList> and <FilterWithSave>: Store User Queries In Preferences

Some lists offer many individual filters and sort options, and users may need to repeatedly apply a certain combination of those - in other words, a custom query. ra-preferences offers users a way to store the current query in local storage, so as to find it later in a list of "saved queries".

`<SavedQueriesList>`

If your list uses the <FilterList> sidebar, add the <SavedQueriesList> component before the first <FilterList> to enable saved queries:

import { FilterList, FilterListItem, List, Datagrid } from 'react-admin';
import { Card, CardContent } from '@material-ui/core';

+import { SavedQueriesList } from '@react-admin/ra-preferences';

const SongFilterSidebar = () => (
    <Card>
        <CardContent>
+           <SavedQueriesList />
            <FilterList label="Record Company" icon={<BusinessIcon />}>
                ...
            </FilterList>
            <FilterList label="Released" icon={<DateRangeeIcon />}>
               ...
            </FilterList>
        </CardContent>
    </Card>
);

const SongList = (props: ListProps) => (
    <List {...props} aside={<SongFilterSidebar />}>
        <Datagrid>
            ...
        </Datagrid>
    </List>
);

`<FilterWithSave>`

If your list uses the <Filter> Button/Form Combo, replace react-admin's <Filter> with ra-preference's <FilterWithSave> to enable saved queries:

import {
-   Filter,
    FilterProps,
    SelectInput,
    DateInput,
    List,
    ListProps,
    Datagrid,
    TextField,
    NumberField,
    DateField
} from 'react-admin';
+import { FilterWithSave } from '@react-admin/ra-preferences';

const SongFilter = (props: FilterProps) => (
-   <Filter {...props}>
+   <FilterWithSave {...props}>
        <SelectInput
            choices={[
                { id: 'Apple', name: 'Apple' },
                { id: 'Atlantic', name: 'Atlantic' },
                { id: 'Capitol', name: 'Capitol' },
                { id: 'Chess', name: 'Chess' },
                { id: 'Columbia', name: 'Columbia' },
                { id: 'DGC', name: 'DGC' },
                { id: 'London', name: 'London' },
                { id: 'Tamla', name: 'Tamla' },
            ]}
            source="recordCompany"
        />
        <DateInput source="released_gte" label="Released after" />
        <DateInput source="released_lte" label="Released before" />
-   </Filter>
+   </FilterWithSave>
);

const SongList = (props: ListProps) => (
    <List {...props} filters={<SongFilter />}>
        <Datagrid rowClick="edit">
            <TextField source="title" />
            <TextField source="artist" />
            <TextField source="writer" />
            <TextField source="producer" />
            <TextField source="recordCompany" />
            <NumberField source="rank" />
            <DateField source="released" />
        </Datagrid>
    </List>
);

Persist User Preferences in Your API

You might want to persist the user preferences in your API so that they get the same Admin accross multiple devices. To do so, your dataProvider must have the following two new methods:

  • getPreferences(): returns a promise which resolves to an object with the following shape: { data: preferences }. Meant to read the user preferences on first load.

  • setPreferences({ data, previousData }): Called when users update their preferences, with data being the new preferences and previousData the preferences before the update. Must returns a promise which resolves to an object with the following shape: { data: preferences }.

Then, you must wrap your Layout inside a <PreferencesContextProvider synchronize>. Note that it accepts a synchronize prop which enable the synchronization of the preferences with the dataProvider.

import { PreferencesContextProvider } from "@react-admin/ra-preferences";
import { Admin, Resource, Layout, LayoutProps } from "react-admin";

const MyLayout = ({ children, ...props }: LayoutProps) => {
  const classes = useStyles();

  return (
    <PreferencesContextProvider synchronize>
      <Layout {...props}>{children}</Layout>
    </PreferencesContextProvider>
  );
};

const App = () => (
  <Admin dataProvider={dataProvider} layout={MyLayout}>
    <Resource name="posts" list={PostList} />
  </Admin>
);
import { PreferencesContextProvider } from "@react-admin/ra-preferences";
import { Admin, Resource, Layout } from "react-admin";

const MyLayout = ({ children, ...props }) => {
  const classes = useStyles();

  return (
    <PreferencesContextProvider synchronize>
      <Layout {...props}>{children}</Layout>
    </PreferencesContextProvider>
  );
};

const App = () => (
  <Admin dataProvider={dataProvider} layout={MyLayout}>
    <Resource name="posts" list={PostList} />
  </Admin>
);

Current limitations

  • There's actually no way to refresh the user preferences. They must logout and login back.
  • Preferences are synchronized at every update.

Using Configurable Components

<List>

This package exposes an alternative <List> component that allows the user to customize the page title and permanent filters. It has the same syntax as react-admin's <List> component.

import { Datagrid, TextField, DateField } from "react-admin";
import { List } from "@react-admin/ra-no-code";

const PostList = (props) => (
  <List {...props}>
    <Datagrid>
      <TextField source="title" />
      <DateField source="date" />
    </Datagrid>
  </List>
);
import { Datagrid, TextField, DateField } from "react-admin";
import { List } from "@react-admin/ra-no-code";

const PostList = (props) => (
  <List {...props}>
    <Datagrid>
      <TextField source="title" />
      <DateField source="date" />
    </Datagrid>
  </List>
);

<Datagrid>

A drop-in replacement for the <Datagrid> component from react-admin, which registers a configuration panel allowing users to show/hide columns.

import { List, TextField, DateField } from "react-admin";
import { Datagrid } from "@react-admin/ra-no-code";

const PostList = (props) => (
  <List {...props}>
    <Datagrid>
      <TextField source="title" />
      <DateField source="date" />
    </Datagrid>
  </List>
);
import { List, TextField, DateField } from "react-admin";
import { Datagrid } from "@react-admin/ra-no-code";

const PostList = (props) => (
  <List {...props}>
    <Datagrid>
      <TextField source="title" />
      <DateField source="date" />
    </Datagrid>
  </List>
);

Tip: You can have multiple configurable Datagrid on a single page (for example in a Dashboard). They'll all have their own section in the configuration sidebar.

Tip: The <Datagrid> configuration is tied to the page on which it is used. If you have multiple datagrids for the same resource on different pages, they won't share the same configuration.

<SimpleList>

A drop-in replacement for the <SimpleList> component from react-admin, which registers a configuration panel allowing users to customize the text displayed for each record.

import { List, TextField, DateField } from "react-admin";
import { SimpleList } from "@react-admin/ra-no-code";

const PostList = (props) => (
  <List {...props}>
    <SimpleList primaryText={(record) => `${record.title} (${record.date})`} />
  </List>
);
import { List } from "react-admin";
import { SimpleList } from "@react-admin/ra-no-code";

const PostList = (props) => (
  <List {...props}>
    <SimpleList primaryText={(record) => `${record.title} (${record.date})`} />
  </List>
);

Users can set the primaryText, secondaryText, and tertiaryText values using the nunjucks templating language. This allows them to write e.g. {{ first_name }} {{ last_name }} for the primaryText value.

<SimpleForm>

A drop-in replacement for the <SimpleForm> component from react-admin, which registers a configuration panel allowing users to change the redirection after save and the UI density.

import { Edit, TextInput, DateInput } from "react-admin";
import { SimpleForm } from "@react-admin/ra-no-code";

const PostEdit = (props) => (
  <Edit {...props}>
    <Simpleform>
      <TextInput source="title" />
      <DateInput source="date" />
    </Simpleform>
  </Edit>
);
import { Edit, TextInput, DateInput } from "react-admin";

const PostEdit = (props) => (
  <Edit {...props}>
    <Simpleform>
      <TextInput source="title" />
      <DateInput source="date" />
    </Simpleform>
  </Edit>
);

Making your own components configurable

In order to make your own components configurable, you'll have to wrap them inside a <PreferencesEditor>. For example, this is how you can register a <DashboardEditor>:

import React, { useRef } from "react";
import { usePreferences } from "@react-admin/ra-preferences";
import {
  PreferencesEditor,
  WithComponentPreferencesEditor,
} from "@react-admin/ra-no-code";
import SimpleStats from "./SimpleStats";
import AdvancedStats from "./AdvancedStats";

const Dashboard = () => {
  const dashboardContainer = useRef();
  const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
    "dashboard.showAdvancedStats",
    false
  );

  return (
    <WithComponentPreferencesEditor
      // This ref is used to display a border making the configurable zone visible to users
      editableEl={dashboardContainer.current}
      editor={<DashboardEditor />}
      openButtonLabel="Configure the Dashboard"
    >
      <div ref={dashboardContainer}>
        {showAdvancedStats ? <AdvancedStats /> : <SimpleStats />}
      </div>
    </WithComponentPreferencesEditor>
  );
};

const DashboardEditor = () => {
  const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
    "dashboard.showAdvancedStats",
    false
  );

  return (
    <PreferencesEditor>
      <FormControlLabel
        control={
          <Switch
            checked={showAdvancedStats}
            onChange={(event, checked) => setShowAdvancedStats(checked)}
            size="small"
          />
        }
        label="Show advanced stats"
      />
    </PreferencesEditor>
  );
};
import React, { useRef } from "react";
import { usePreferences } from "@react-admin/ra-preferences";
import {
  PreferencesEditor,
  WithComponentPreferencesEditor,
} from "@react-admin/ra-no-code";
import SimpleStats from "./SimpleStats";
import AdvancedStats from "./AdvancedStats";

const Dashboard = () => {
  const dashboardContainer = useRef();
  const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
    "dashboard.showAdvancedStats",
    false
  );

  return (
    <WithComponentPreferencesEditor
      // This ref is used to display a border making the configurable zone visible to users
      editableEl={dashboardContainer.current}
      editor={<DashboardEditor />}
      openButtonLabel="Configure the Dashboard"
    >
      <div ref={dashboardContainer}>
        {showAdvancedStats ? <AdvancedStats /> : <SimpleStats />}
      </div>
    </WithComponentPreferencesEditor>
  );
};

const DashboardEditor = () => {
  const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
    "dashboard.showAdvancedStats",
    false
  );

  return (
    <PreferencesEditor>
      <FormControlLabel
        control={
          <Switch
            checked={showAdvancedStats}
            onChange={(event, checked) => setShowAdvancedStats(checked)}
            size="small"
          />
        }
        label="Show advanced stats"
      />
    </PreferencesEditor>
  );
};

Tip: If users should be able to set a preference on per page basis, you should include the current location in the preference key:

import { useLocation } from "react-router";
import { usePreferences } from "@react-admin/ra-preferences";

const StatisticsEditor = () => {
  const location = useLocation();
  const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
    `${location.pathname}.statistics`,
    false
  );

  // ...
};
import { useLocation } from "react-router";
import { usePreferences } from "@react-admin/ra-preferences";

const StatisticsEditor = () => {
  const location = useLocation();
  const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
    `${location.pathname}.statistics`,
    false
  );

  // ...
};

API

<WithInspector>

This component creates a PreferencesEditorContext, required to handle the configuration dialog display, and adds the configuration dialog component to its child (which must be a react-admin <Dialog>.

You have to wrap your Layout inside this component:

// in src/MyLayout.js
import { Layout, LayoutProps } from "react-admin";
import { WithInspector } from "@react-admin/ra-no-code";

export const MyLayout = (props: LayoutProps) => (
  <WithInspector {...props}>
    <Layout {...props} />
  </WithInspector>
);
// in src/MyLayout.js
import { Layout } from "react-admin";
import { WithInspector } from "@react-admin/ra-no-code";

export const MyLayout = (props) => (
  <WithInspector {...props}>
    <Layout {...props} />
  </WithInspector>
);

<PreferencesEditorButton>

A component that displays a Material-UI <IconButton> and opens the <PreferencesEditorDrawer> when clicked.

We advise you to put it inside your <Appbar>:

// in src/MyAppbar.js
import { AppBar, AppBarProps } from "react-admin";
import { Typography } from "@material-ui/core";
import { PreferencesEditorButton } from "@react-admin/ra-no-code";

export const MyAppbar = (props: AppBarProps) => (
  <AppBar {...props}>
    <Typography variant="h6" color="inherit" id="react-admin-title" />
    <span style={{ flex: 1 }} />
    // This button is required for users to open the preferences editor sidebar
    <PreferencesEditorButton />
  </AppBar>
);
// in src/MyAppbar.js
import { AppBar } from "react-admin";
import { Typography } from "@material-ui/core";
import { PreferencesEditorButton } from "@react-admin/ra-no-code";

export const MyAppbar = (props) => (
  <AppBar {...props}>
    <Typography variant="h6" color="inherit" id="react-admin-title" />
    <span style={{ flex: 1 }} />
    // This button is required for users to open the preferences editor sidebar
    <PreferencesEditorButton />
  </AppBar>
);

<WithComponentPreferencesEditor>

A component displaying a border around configurable components, as well as a floating button to show an editor specific to this component. For example:

import React, { useRef } from "react";
import { usePreferences } from "@react-admin/ra-preferences";
import {
  PreferencesEditor,
  WithComponentPreferencesEditor,
} from "@react-admin/ra-no-code";
import SimpleStats from "./SimpleStats";
import AdvancedStats from "./AdvancedStats";

const Dashboard = () => {
  const dashboardContainer = useRef();
  const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
    "dashboard.showAdvancedStats",
    false
  );

  return (
    <WithComponentPreferencesEditor
      // This ref is used to display a border making the configurable zone visible to users
      editableEl={dashboardContainer.current}
      editor={<DashboardEditor />}
      openButtonLabel="Configure the Dashboard"
    >
      <div ref={dashboardContainer}>
        {showAdvancedStats ? <AdvancedStats /> : <SimpleStats />}
      </div>
    </WithComponentPreferencesEditor>
  );
};

const DashboardEditor = () => {
  const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
    "dashboard.showAdvancedStats",
    false
  );

  return (
    <PreferencesEditor>
      <FormControlLabel
        control={
          <Switch
            checked={showAdvancedStats}
            onChange={(event, checked) => setShowAdvancedStats(checked)}
            size="small"
          />
        }
        label="Show advanced stats"
      />
    </PreferencesEditor>
  );
};
import React, { useRef } from "react";
import { usePreferences } from "@react-admin/ra-preferences";
import {
  PreferencesEditor,
  WithComponentPreferencesEditor,
} from "@react-admin/ra-no-code";
import SimpleStats from "./SimpleStats";
import AdvancedStats from "./AdvancedStats";

const Dashboard = () => {
  const dashboardContainer = useRef();
  const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
    "dashboard.showAdvancedStats",
    false
  );

  return (
    <WithComponentPreferencesEditor
      // This ref is used to display a border making the configurable zone visible to users
      editableEl={dashboardContainer.current}
      editor={<DashboardEditor />}
      openButtonLabel="Configure the Dashboard"
    >
      <div ref={dashboardContainer}>
        {showAdvancedStats ? <AdvancedStats /> : <SimpleStats />}
      </div>
    </WithComponentPreferencesEditor>
  );
};

const DashboardEditor = () => {
  const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
    "dashboard.showAdvancedStats",
    false
  );

  return (
    <PreferencesEditor>
      <FormControlLabel
        control={
          <Switch
            checked={showAdvancedStats}
            onChange={(event, checked) => setShowAdvancedStats(checked)}
            size="small"
          />
        }
        label="Show advanced stats"
      />
    </PreferencesEditor>
  );
};

It accepts the following props:

  • editableEl: A React ref for the component that should be highlighted when the configuration mode is enabled. The component will be highlighted, and the floating configuration button will be displayed in the top right or left corner.
  • editor: The element containing the configuration UI for this component.
  • openButtonLabel: The label shown as a tooltip for the button opening the configuration for this component. Accepts a translation key.

<PreferencesEditor>

This component ensures a correct navigation behavior for all editors when they are displayed inside the <PreferencesEditorDialog>. It also provides an in memory router for complex editors that can render a react-router <Switch> with custom routes. The <PreferencesEditor> will handle the display and behavior of the back button inside the <PreferencesEditorDialog>.

Here's an example of such complex editor:

import React, { useRef } from 'react';
import { usePreferences } from '@react-admin/ra-preferences';
import { PreferencesEditor } from '@react-admin/ra-no-code';
import { Switch, Route, Link } from 'react-router-dom';
import { AdvancedSettings } from './AdvancedSettings';

const DashboardEditor = () => {
    const [showAdvancedStats, setShowAdvancedStats] = usePreferences(
        'dashboard.showAdvancedStats',
        false
    );

    return (
        <PreferencesEditor>
            <Switch>
                <Route path="advanced" render={() => <AdvancedSettings />} />
                <Route path="/" render={() => (
                    <FormControlLabel
                        control={
                            <Switch
                                checked={showAdvancedStats}
                                onChange={(event, checked) => setShowAdvancedStats(checked)}
                                size="small"
                            />
                        }
                        label="Show advanced stats"
                    />
                    <Button component={Link} to="/advanced">Advanced settings</Button>
                )} />
            </Switch>
        </PreferencesEditor>
    );
};

CHANGELOG

v1.4.2

2021-09-13

  • (fix) Fix no-code editors are not available after navigating
  • (fix) Fix list default filter editor for booleans should be applied immediately
  • (fix) Fix no-code screencast and full app story

v1.4.1

2021-09-09

  • (fix) Update no-code components for react-admin 3.18

v1.4.0

2021-09-03

  • (feat) Introduce no-code components which allows users to configure parts of the UI to better suits their needs.

v1.3.7

2021-08-02

  • (fix) Fix warnings about keys when using useSelectedColumns.

v1.3.6

2021-07-21

  • (fix) Fix preferences synchronization accross browser tabs.

v1.3.5

2021-07-08

  • (fix) Fix handling of undefined values when setting preference on a key.

v1.3.4

2021-06-29

  • (fix) Fix SidebarOpenPreferenceSync resets its value on each page load

v1.3.3

2021-06-29

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

v1.3.2

2021-06-24

  • (fix) Fix library exports (<PreferencesContextProvider> and related hooks)

v1.3.1

2021-06-15

  • (fix) Fix regression in usePreferences preventing to set a preference value back to its default.

v1.3.0

2021-05-25

  • (feat) Add optional synchronization of the preferences with the API.

To enable this feature, your dataProvider must have the following two methods:

  • getPreferences(): returns a promise which resolves to an object with the following shape: { data: preferences }. Meant to read the user preferences on first load.

  • setPreferences({ data, previousData }): Called when users update their preferences, with data being the new preferences and previousData the preferences before the update. Must returns a promise which resolves to an object with the following shape: { data: preferences }.

You must wrap your Layout inside a <PreferencesContextProvider synchronize>. Note that it accepts a synchronize prop which enable the synchronization of the preferences with the dataProvider.

import { PreferencesContextProvider } from "@react-admin/ra-preferences";
import { Admin, Resource, Layout } from "react-admin";

const MyLayout = ({ children, ...props }) => {
  const classes = useStyles();

  return (
    <PreferencesContextProvider synchronize>
      <Layout {...props}>{children}</Layout>
    </PreferencesContextProvider>
  );
};

const App = () => (
  <Admin dataProvider={dataProvider} layout={MyLayout}>
    <Resource name="posts" list={PostList} />
  </Admin>
);
import { PreferencesContextProvider } from "@react-admin/ra-preferences";
import { Admin, Resource, Layout } from "react-admin";

const MyLayout = ({ children, ...props }) => {
  const classes = useStyles();

  return (
    <PreferencesContextProvider synchronize>
      <Layout {...props}>{children}</Layout>
    </PreferencesContextProvider>
  );
};

const App = () => (
  <Admin dataProvider={dataProvider} layout={MyLayout}>
    <Resource name="posts" list={PostList} />
  </Admin>
);

v1.2.6

2021-05-25

  • (fix) useSelectedColumns now filters out columns stored in preferences but not available anymore.

v1.2.5

2021-04-26

  • (performances) Replace MUI boxes by div with styles.

v1.2.4

2021-04-19

  • (fix) Fallback to inmemory storage when localstorage is unavailable

v1.2.3

2021-02-01

  • (fix) Fix Fallback on default value in usePreferences

v1.2.2

2021-01-22

  • (fix) Fix SidebarOpenPreferenceSync

v1.2.1

2021-01-21

  • (fix) Fix usePreferences ignore falsy values
  • (fix) Fix SidebarOpenPreferenceSync initialization

v1.2.0

2020-12-07

  • Add <FilterWithSave> component, to allow persisted queries in <Filter>
  • Add <SavedQueriesList> component, to allow persisted queries in <FilterList>
  • (fix) translate ToggleThemeButton tooltip

v1.1.4

2020-11-17

  • (fix) Remove useless defaultLanguage prop in <LanguageSwitcher>

v1.1.3

2020-11-06

  • (fix) Fix SelectColumnsMenu Unnecessary Props

v1.1.2

2020-11-06

  • (fix) Fix SelectColumnsMenu Props Types

v1.1.1

2020-10-31

  • (fix) PreferenceBasedThemeProvider passes a complete MuiTheme to <Admin> instead of a simple object

v1.1.0

2020-10-05

  • Upgrade to react-admin 3.9

v1.0.2

2020-09-30

  • Update Readme

v1.0.1

2020-09-16

  • (deps) Upgrade dependencies

v1.0.0

2020-07-31

  • First release