React Admin v3 Advanced Recipes: Managing User Profile

Gildas Garcia
Gildas GarciaDecember 14, 2020
#react-admin#react#tutorial

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.

User profile edition

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.

Edit profile page

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!

User profile edition

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:

User profile edition

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.

Did you like this article? Share it!