React Admin Advanced Recipes: User Profile

React Admin Advanced Recipes: User Profile

Gildas Garcia
• 7 min read

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:

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:

schema about wrapping the dataProvider

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 it profile
  • 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 an id 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 or PATCH) request to be made on a https://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.

Edit profile page

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!

End result

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

Gildas Garcia

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.

Comments