React Admin v3 Advanced Recipes: Managing User Profile
This is an update of the first article in the series of advanced tutorials for React-admin, updated for v3. It assumes you already understand the basics of react-admin
and have at least completed the official tutorial.
In the previous article, I explained how to create a profile page based on an <Edit>
component, and accessible as a standalone page.
Indeed, it is sometimes necessary to store some global data about the current user and their preferences for the application. Adding a resource for a profile
would lead to having a <List>
page which would not make sense from a user point of vue.
In this article, we'll rebuild this functionality from scratch, leveraging the new hooks from react-admin v3.
You can see the final code in the following codesandbox.
Handling the Data
This part doesn't change much from the previous article, as we still want to leverage react-admin features: easy forms with validation, optimistic UI, notifications, error handling, etc.
Likewise, for the purpose of this article, I decided to store the profile in the browser local storage.
However, I'll leverage the improved support for custom methods on the dataProvider
and the new authProvidergetIdentify
method.
First, I introduce two new dataProvider
custom methods: getUserProfile
and updateUserProfile
.
// A function decorating a dataProvider for handling user profiles
const addUserProfileOverrides = dataProvider => ({
...dataProvider,
getUserProfile(params) {
const storedProfile = localStorage.getItem("profile");
if (storedProfile) {
return Promise.resolve({
data: JSON.parse(storedProfile),
});
}
// No profile yet, return a default one
return Promise.resolve({
data: {
// As we are only storing this information in the localstorage, we don't really care about this id
id: 'unique-id',
fullName: "",
avatar: ""
},
});
},
updateUserProfile(params) {
localStorage.setItem("profile", JSON.stringify({ id: 'unique-id', ...params.data }));
return Promise.resolve({ data: params.data });
}
});
export default addUserProfileMethods(
jsonRestProvider(data)
);
Editing the Profile
Now that the dataProvider
is ready, I must set up the UI to edit the profile. I won't use the Edit
component as there is no profile
resource. However, I can leverage some of its building block to still enjoy some of the edition features such as form validation, notifications, etc.
// in profile.js
import React, { useCallback, useMemo } from 'react';
import { FileInput, TextInput, SimpleForm, required } from 'react-admin';
export const ProfileEdit = ({ staticContext, ...props }) => {
const dataProvider = useDataProvider();
const notify = useNotify();
const [saving, setSaving] = useState();
const { loaded, identity } = useGetIdentity();
const handleSave = useCallback((values) => {
setSaving(true);
dataProvider.updateUserProfile(
{ data: values },
{
onSuccess: ({ data }) => {
setSaving(false);
notify("Your profile has been updated", "info", {
_: "Your profile has been updated"
});
},
onFailure: () => {
setSaving(false);
notify(
"A technical error occured while updating your profile. Please try later.",
"warning",
{
_:
"A technical error occured while updating your profile. Please try later."
}
);
}
}
);
}, [dataProvider, notify, refresh]);
const saveContext = useMemo(() => ({
save: handleSave,
saving
}), [saving, handleSave]);
if (!user.loaded) {
return null;
}
return (
<SaveContextProvider value={saveContext}>
<SimpleForm save={handleSave} record={identity ? identity : {}}>
<TextInput source="fullName" validate={required()} />
<FileInput source="avatar" validate={required()} />
</SimpleForm>
</SaveContextProvider>
);
};
In order to allow users to visit their profile page, I then add a custom route, which must be set on the <Admin>
component:
+import { Route } from "react-router";
+import { ProfileEdit } from './profile';
const App = () => (
<Admin
dataProvider={dataProvider}
+ customRoutes={[
+ <Route
+ key="my-profile"
+ path="/my-profile"
+ component={ProfileEdit}
+ />
+ ]}
>
<Resource name="posts" {...posts} />
<Resource name="comments" {...comments} />
<Resource name="tags" {...tags} />
</Admin>
);
I can now verify that it works by accessing the /my-profile
path by typing its url directly.
Accessing the Profile Page
Most apps provide access to the profile through a user menu in the app bar. By default, React-admin adds a user menu with a single logout entry only if there is an authenticated user.
However, it is easy to customize the user menu entries by supplying my own user menu and app bar components in a custom layout.
I also fetch the user profile in order to display its nickname:
// in src/MyUserMenu.js
import React from "react";
import { UserMenu, MenuItemLink } from "react-admin";
import SettingsIcon from "@material-ui/icons/Settings";
const MyUserMenu = (props) => {
= (props) => {
return (
<UserMenu {...props}>
<MenuItemLink
to="/my-profile"
primaryText="My Profile"
leftIcon={<SettingsIcon />}
/>
</UserMenu>
);
};
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!
However, there's an issue. When users update their profile, react-admin doesn't refresh it and will not update the user name in the menu. We need a way to force this update.
Refreshing the Profile Data
At first, we might want to use the version
mechanism provided by react-admin
. However, it would mean that the profile would be refetched every time I update any of the resources, and that is not what I want. Instead, I will replicate this mechanism but ony for the profile.
There are two possible ways for this. I can either use a custom redux reducer or leverage react contexts. Let's do it using context.
First, I introduce a new context and its provider, which should provide the profile version and a way to update it:
// in profile.js
const ProfileContext = createContext();
export const ProfileProvider = ({ children }) => {
const [profileVersion, setProfileVersion] = useState(0);
const context = useMemo(
() => ({
profileVersion,
refreshProfile: () =>
// Increment the version which will update the state
setProfileVersion((currentVersion) => currentVersion + 1)
}),
[profileVersion]
);
return (
<ProfileContext.Provider value={context}>
{children}
</ProfileContext.Provider>
);
};
Then, in the ProfileEdit
, I call the refreshProfile
function as a side effect of a successful update:
export const ProfileEdit = ({ staticContext, ...props }) => {
const dataProvider = useDataProvider();
const notify = useNotify();
const [saving, setSaving] = useState();
+ const { refreshProfile } = useProfile();
const { loaded, identity } = useGetIdentity();
const handleSave = useCallback((values) => {
setSaving(true);
dataProvider.updateUserProfile(
{ data: values },
{
onSuccess: ({ data }) => {
setSaving(false);
notify("Your profile has been updated", "info", {
_: "Your profile has been updated"
});
+ refreshProfile();
},
onFailure: () => {
setSaving(false);
notify(
"A technical error occured while updating your profile. Please try later.",
"warning",
{
_:
"A technical error occured while updating your profile. Please try later."
}
);
}
}
);
}, [dataProvider, notify, refresh]);
const saveContext = useMemo(() => ({
save: handleSave,
saving
}), [saving, handleSave]);
if (!user.loaded) {
return null;
}
return (
<SaveContextProvider value={saveContext}>
<SimpleForm save={handleSave} record={identity ? identity : {}}>
<TextInput source="fullName" validate={required()} />
<FileInput source="avatar" validate={required()} />
</SimpleForm>
</SaveContextProvider>
);
};
Finally, I update the UserMenu
so that it rerenders whenever the profile version changes:
import React from "react";
import { useAuthState, UserMenu, MenuItemLink } from "react-admin";
import SettingsIcon from "@material-ui/icons/Settings";
+import { useProfile } from "./profile";
const MyUserMenu = (props) => {
const { authenticated } = useAuthState();
return authenticated ? <AuthenticatedUserMenu {...props} /> : null;
};
const AuthenticatedUserMenu = (props) => {
+ const { profileVersion } = useProfile();
return (
- <UserMenu {...props}>
+ <UserMenu key={version} {...props}>
<MenuItemLink
to="/my-profile"
primaryText="My Profile"
leftIcon={<SettingsIcon />}
/>
</UserMenu>
);
};
export default MyUserMenu;
Now it works as expected:
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.