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 profiles
const 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.jso
import 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.js
import 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 itprofile
basePath
: 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 anid
as 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 (POST
orPATCH
) request to be made on ahttps://api.com/me
route for example. That means I can pass it whatever I like,my-profile
for 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.js
import 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.js
import React from 'react';
import { AppBar } from 'react-admin';
import MyUserMenu from './MyUserMenu';
const MyAppBar = props => <AppBar {...props} userMenu={<MyUserMenu />} />;
export default MyAppBar;
// in ./MyLayout.js
import 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.