Preferences
Ra-core contains a global, synchronous, persistent store for storing user preferences. Think of the Store as a key-value database that persists between page loads.
Users expect that UI choices, like changing the interface language or theme, should only be made once. Let’s call these choices “preferences”. The ra-core Store is the perfect place to store preferences.
The store uses the browser local storage (or a memory storage when localStorage
isn’t available). The store is emptied when the user logs out.
It requires no setup, and is available via the useStore
hook.
Ra-core provides the following hooks to interact with the Store:
Some ra-core components use the Store internally. For example the list controller stores the list parameters (like pagination and filters) in it.
For instance, here is how to use it to show or hide a help panel:
import { useStore } from 'ra-core';
const HelpButton = () => { const [helpOpen, setHelpOpen] = useStore('help.open', false); return ( <div> <button onClick={() => setHelpOpen(v => !v)}> {helpOpen ? 'Hide' : 'Show'} help </button> {helpOpen && ( <div style={{ position: 'absolute', background: 'white', border: '1px solid #ccc', padding: '1rem', borderRadius: '4px' }}> Help content goes here </div> )} </div> );};
Store-Based Hooks
Section titled “Store-Based Hooks”Ra-core components don’t access the store directly ; instead, they use purpose-driven hooks, which you can use, too:
useLocaleState()
for the localeuseUnselect()
,useUnselectAll()
,useRecordSelection()
for the selected records for a resourceuseExpanded()
for the expanded rows in a datatable
Using specialized hooks avoids depending on a store key.
Forward Compatibility
Section titled “Forward Compatibility”If you store complex objects in the Store, and you change the structure of these objects in the application code, the new code relying on the new object structure may fail when running with an old stored object.
For instance, let’s imagine an app storing a User Preferences object in the Store under the 'preferences'
key. The object looks like:
{ fontSize: 'large', colorScheme: 'dark' }
Then, the developer changes the structure of the object:
{ ui: { fontSize: 'large', mode: 'dark', }}
The new code reads the preferences from the Store and expects the value to respect the new structure:
const preferences = useStore('preferences');// this will throw an error if a user has an old preferences objectconst { fontSize, mode } = preferences.ui;
To avoid this type of error, the code using the Store should always make sure that the object from the Store has the expected structure, and use a default value if not. To put it otherwise, always assume that the data from the store may have the wrong shape - it’s the only way to ensure forward compatibility.
let preferences = useStore('preferences');if (!preferences.ui || !preferences.ui.fontSize || !preferences.ui.mode) { preferences = { ui: { fontSize: 'large', mode: 'dark' } };}// this will never failconst { fontSize, mode } = preferences.ui;
You may want to use libraries that validate the schema of an object, like Yup, Zod, Superstruct, or Joi.
Even better: don’t store objects in the Store at all, only store scalar values instead. You can call useStore
several times:
let fontSize = useStore('preferences.ui.fontSize');let mode = useStore('preferences.ui.mode');
Store Invalidation
Section titled “Store Invalidation”If your application cannot check the shape of a stored object, ra-core provides an escape hatch to avoid errors for users with an old value: store invalidation.
The idea is that you can specify a version number for your Store. If the Store contains data with a different version number than the code, the Store resets all preferences.
To create a Store with a different version number, call the localStorageStore()
function with a version identifier, then pass the resulting object as the <CoreAdmin store>
prop:
import { CoreAdmin, Resource, localStorageStore } from 'ra-core';
const STORE_VERSION = "2";
const App = () => ( <CoreAdmin dataProvider={dataProvider} store={localStorageStore(STORE_VERSION)}> <Resource name="posts" /> </CoreAdmin>);
Increase the version number each time you push code that isn’t compatible with the stored values.
Share/separate Store data between same domain instances
Section titled “Share/separate Store data between same domain instances”If you are running multiple instances of ra-core applications on the same domain, you can distinguish their stored objects by defining different application keys. By default, the application key is empty to allow configuration sharing between instances.
import { CoreAdmin, Resource, localStorageStore } from 'ra-core';
const APP_KEY = 'blog';
const App = () => ( <CoreAdmin dataProvider={dataProvider} store={localStorageStore(undefined, APP_KEY)}> <Resource name="posts" /> </CoreAdmin>);
Transient Store
Section titled “Transient Store”If you don’t want the store to be persisted between sessions, you can override the default <CoreAdmin store>
component:
import { CoreAdmin, Resource, memoryStore } from 'ra-core';
const App = () => ( <CoreAdmin dataProvider={dataProvider} store={memoryStore()}> <Resource name="posts" /> </CoreAdmin>);
This way, each time the application is loaded, the store will be reset to an empty state.
Testing Components Using The Store
Section titled “Testing Components Using The Store”The ra-core Store is persistent. This means that if a unit test modifies an item in the store, the value will be changed for the next test. This will cause random test failures when you use useStore()
in your tests, or any feature depending on the store (e.g. row selection, sidebar state, language selection).
To isolate your unit tests, pass a new memoryStore
for each test:
import { CoreAdminContext, memoryStore } from 'ra-core';
test('<MyComponent>', async () => { const { getByText } = render( <CoreAdminContext store={memoryStore()}> <MyComponent /> </CoreAdminContext> ); const items = await screen.findAllByText(/Item #[0-9]: /) expect(items).toHaveLength(10)})
If you don’t need <CoreAdminContext>
, you can just wrap your component with a <StoreContextProvider>
:
import { StoreContextProvider, memoryStore } from 'ra-core';
test('<MyComponent>', async () => { const { getByText } = render( <StoreContextProvider value={memoryStore()}> <MyComponent /> </StoreContextProvider> ); const items = await screen.findAllByText(/Item #[0-9]: /) expect(items).toHaveLength(10)})