React Admin Advanced Recipes: User Profile
This is the fourth article in the series of advanced tutorials for React-admin. It assumes you already understand the basics of react-admin and have at least completed the official tutorial.
Tip: This article was written for react-admin v2. If you’re working on a recent react-admin project, you probably want to read the same tutorial updated for react-admin v3.
Sometimes, it is necessary to store some global data about the current user and their preferences for the application.
I could add a react-admin Resource for a profile, but from the user’s point of view, it does not make sense to access their profile through a list page with a single record and an “edit” button. After all, they will only ever have one profile to edit.
In this article, I’ll explain how to create a profile page based on an <Edit> component, and accessible as a standalone page. As usual, I’ll work on a fork of the codesandbox that was built in the last article.
Below is a screen-cast of the end result:

Handling the Data
First, I have to decide how I will store and load the user’s profile. For the purpose of this article, let’s store it in the browser local storage.
However, I still want to use the dataProvider to load and update it. Indeed, this will allow me to use all the niceties react-admin provides for my resources: easy forms with validation, notifications, error handling, etc.
For that, I need the profile to be a react-admin resource, and to have an id. I’m sure there will only be a single profile per user. It means I only need to handle the GET_ONE and UPDATE fetch types.
All I have to do is to make the dataProvider handle this special case. The most straightforward way to do it is to decorate the dataProvider, giving me a chance to intercept calls related to the profile resource:

import jsonRestProvider from "ra-data-fakerest";import { GET_ONE, UPDATE } from "react-admin";import data from "./data";
const disableFakeFetchRequestsLogs = true;
// A function decorating a dataProvider for handling user profilesconst handleUserProfile = (dataProvider) => (verb, resource, params) => { // I know I only GET or UPDATE the profile as there is only one for the current user // To showcase how I can do something completely different here, I'll store it in local storage // You can replace this with a customized fetch call to your own API route, too if (resource === "profile") { if (verb === GET_ONE) { const storedProfile = localStorage.getItem("profile");
if (storedProfile) { return Promise.resolve({ data: JSON.parse(storedProfile), }); }
// No profile yet, return a default one // It's important that I send the same id as requested in params. // Indeed, react-admin will verify it and may throw an error if they are different // I don't have to do it when the profile exists as it will be included in the data stored in the local storage return Promise.resolve({ data: { id: params.id, nickname: "" }, }); }
if (verb === UPDATE) { localStorage.setItem("profile", JSON.stringify(params.data)); return Promise.resolve({ data: params.data }); } }
// Fallback to the dataProvider default handling for all other resources return dataProvider(verb, resource, params);};
export default handleUserProfile( jsonRestProvider(data, disableFakeFetchRequestsLogs),);I can now declare the profile resource in my <Admin />.
const App = () => ( <Admin dataProvider={dataProvider} > <Resource name="posts" {...posts} /> <Resource name="comments" {...comments} /> <Resource name="tags" {...tags} /> <Resource name="profile" /> </Admin>);Editing the Profile
Now that my dataProvider is ready, I must set up the UI to edit the profile. I can follow the usual workflow here, starting with the edit component:
// in profile/EditProfile.jsoimport React from 'react';import { Edit, TextInput, SimpleForm, required } from 'react-admin';
const ProfileEdit = ({ staticContext, ...props }) => { return ( <Edit redirect={false} // I don't need any redirection here, there's no list page {...props} > <SimpleForm> <TextInput source="nickname" validate={required()} /> </SimpleForm> </Edit> );};
export default ProfileEdit;
// in profile/index.jsimport ProfileEdit from './ProfileEdit';
export default { edit: ProfileEdit};Nothing fancy here. But now, how can I provide a way for my users to access this view? By default, React-admin won’t include an entry in the menu for resources without a list component.
Fortunately, React-admin supports custom routes, which must be set on the <Admin> component:
import profile from './profile';
const App = () => ( <Admin dataProvider={dataProvider} customRoutes={[ <Route key="my-profile" path="/my-profile" component={profile.edit} /> ]} > <Resource name="posts" {...posts} /> <Resource name="comments" {...comments} /> <Resource name="tags" {...tags} /> <Resource name="profile" /> </Admin>);However, as the route hasn’t been created by a <Resource> component, it won’t receive the props required by the <Edit> component to do its work: loading and saving a resource specified by its id.
There are 3 such props:
resource: This one is easy, I can just pass itprofilebasePath: This is used for the redirection which happen after the resource is saved. I don’t really need it as I disabled the redirection but the component expects it. I’ll pass it the custom page path/my-profile.id: This one is trickier. I don’t really have anidas I store the profile in local storage and if I had one (from the database), my api might not expose it and just expect the loading (GET) and update (POSTorPATCH) request to be made on ahttps://api.com/meroute for example. That means I can pass it whatever I like,my-profilefor example.
Let’s fix the EditProfile component:
const ProfileEdit = ({ staticContext, ...props }) => { return ( <Edit /* As this component isn't loaded from a route generated by a <Resource>, I have to provide the id myself. As there is only one config for the current user, I decided to hard-code it here */ id="my-profile" /* For the same reason, I need to provide the resource and basePath props which are required by the Edit component */ resource="profile" basePath="/my-profile" redirect={false} /* I also customized the page title as it'll make more sense to the user */ title="My profile" {...props} > <SimpleForm> <TextInput source="nickname" validate={required()} /> </SimpleForm> </Edit> );};I can now verify that it works by accessing the /my-profile path by myself.

However, I still don’t have provided my users a way to access it.
Accessing the Profile Page
Most apps provide access to the profile through a user menu in the app bar. By default, React-admin will add a user menu with a single logout entry only if there is an authenticated user.
However, it is easy to customize it by supplying my own user menu and app bar components in a custom layout.
I will also fetch the user profile in order to display its nickname:
// in src/MyUserMenu.jsimport React, { Component } from 'react';import { connect } from 'react-redux';import Typography from '@material-ui/core/Typography';import { crudGetOne, UserMenu, MenuItemLink } from 'react-admin';import SettingsIcon from '@material-ui/icons/Settings';
class MyUserMenuView extends Component { componentDidMount() { this.fetchProfile(); }
fetchProfile = () => { this.props.crudGetOne( // The resource 'profile', // The id of the resource item to fetch 'my-profile', // The base path. Mainly used on failure to fetch the data '/my-profile', // Whether to refresh the current view. I don't need it here false ); };
render() { const { crudGetOne, profile, ...props } = this.props;
return ( <UserMenu label={profile ? profile.nickname : ''} {...props}> <MenuItemLink to="/configuration" primaryText="Configuration" leftIcon={<SettingsIcon />} /> </UserMenu> ); }}
const mapStateToProps = state => { const resource = 'profile'; const id = 'my-profile'; const profileState = state.admin.resources[resource];
return { profile: profileState ? profileState.data[id] : null };};
const MyUserMenu = connect( mapStateToProps, { crudGetOne })(MyUserMenuView);export default MyUserMenu;
// in src/MyAppBar.jsimport React from 'react';import { AppBar } from 'react-admin';import MyUserMenu from './MyUserMenu';
const MyAppBar = props => <AppBar {...props} userMenu={<MyUserMenu />} />;export default MyAppBar;
// in ./MyLayout.jsimport React from 'react';import { Layout } from 'react-admin';import MyAppBar from './MyAppBar';
const MyLayout = props => <Layout {...props} appBar={MyAppBar} />;
export default MyLayout;Finally, I have to instruct the Admin component to use my layout:
import MyLayout from './MyLayout';
const App = () => ( <Admin dataProvider={dataProvider} customRoutes={[ <Route key="my-profile" path="/my-profile" component={profile.edit} /> ]} appLayout={MyLayout} > <Resource name="posts" {...posts} /> <Resource name="comments" {...comments} /> <Resource name="tags" {...tags} /> <Resource name="profile" /> </Admin>);And voila, I now have a profile page leveraging react-admin features!

Conclusion
The core team wondered whether it would be a good idea to provide defaults for this kind of feature. However, as all applications would probably have different needs for it, we instead ensured react-admin will never get in your way. This is one of many examples.
As usual, you’ll find the code for this tutorial in the following codesandbox.
Authors
Full-stack web developer at marmelab, Gildas has a strong appetite for emerging technologies. If you want an informed opinion on a new library, ask him, he's probably used it on a real project already.