How to conditionally render React UI based on user permissions

8 minute(s) read

Web Applications become more and more complex thanks to the power and capabilities of modern browsers. With bigger and broader applications rises the need of authentication and segmentation of users.

In fact, different users have different rights on the application, allowing them to perform different actions according to their profile/role.

In this article we will learn how to organise the permissions and how to display part of your application based on the permissions given to the current user.

TL;DR;

If you are more of the show-me-the-code type of developer, you can find the final code here: https://github.com/francois-roget/permission-provider-demo

This is not security!

First of all, I would like to stress that this mechanism is not intended to prevent unauthorised requests to the server. It is more of a user experience improvement.

We have to keep in mind that any JavaScript code running on the browser is present locally and completely readable by the end user. Hiding a button or a screen with this technique will not prevent anybody to look at the code and discover the API endpoints or the internal logic of the application.

All calls made to the API should be checked, on the server side, for user’s permissions before taking any action.

How to organise your permissions

The more naïve/tempting way to deal with user permission is to check, for example, if the current user is “admin”. Based on this, different parts of the application would be allowed or not.

Doing this will quickly block you as new requests will certainly arrive to change what a (non-)admin user can see or to create another type of user that can do more than a simple user but less than an admin.

The best way to deal with permissions is to create one permission per granular action that would be allowed or restricted in the application. For example, you can define separated permissions for:

  • Viewing a list of elements (element.list)
  • Adding an element (element.add)
  • Modifying an element (element.edit)
  • Removing an element (element.delete)

The idea would be then to aggregate those permissions into roles where you define what users assigned to that role can do. For example, creating a role “admin” with all the permissions would be a good starting point. You could then create a role “viewer” with only element.list as permissions and a role “contributor” with element.list and element.add.

Examples in the code at the github link take a simpler approach of assigning individual permissions directly to the users. In you real-world application, please consider grouping them in roles.

Simple use case

Let’s image a simple CRUD use case where the logged user has different actions available according to their roles:

  • The Viewer role can only list the items
  • The Contributor role can list items and add new ones
  • The Administrator role can do all actions (list items, add an item and delete an item)

Minimal Viable Product

Now let’s see the minimal code that we need to implement in order to have the required features. First of all we will use the React’s Context API. This api allows the developer to create a Context that is composed of 2 elements:

  • A Provider that will hold the data
  • A Consumer that will consume the data provided by the Provider

Schema 1

In this case, the data passed between the provider and the consumer is a method allowing the consumer to know if a particular permission is granted to the current user.

import React from 'react';
import {Permission} from "../Types";

type PermissionContextType = {
    isAllowedTo: (permission: Permission) => boolean;
}

// Default behaviour for the Permission Provider Context
// i.e. if for whatever reason the consumer is used outside of a provider.
// The permission will not be granted unless a provider says otherwise
const defaultBehaviour: PermissionContextType = {
    isAllowedTo: () => false
}

// Create the context
const PermissionContext = React.createContext<PermissionContextType>( defaultBehaviour);

export default PermissionContext;

Next we should implement the PermissionProvider that will hold the logic of checking the user permission. This PermissionProvider will receive the user’s permissions as a prop and provide the implementation of the method isAllowedTo.

import React from 'react';
import {Permission} from "../Types";
import PermissionContext from "./PermissionContext";

type Props = {
    permissions: Permission[]
}

// This provider is intended to be surrounding the whole application.
// It should receive the users permissions as parameter
const PermissionProvider: React.FunctionComponent<Props> = ({permissions, children}) => {

    // Creates a method that returns whether the requested permission is available in the list of permissions
    // passed as parameter
    const isAllowedTo = (permission: Permission) => permissions.includes(permission);

    // This component will render its children wrapped around a PermissionContext's provider whose
    // value is set to the method defined above
    return <PermissionContext.Provider value=>{children}</PermissionContext.Provider>;
};

export default PermissionProvider;

The last component we need is a consumer to use inside our application at every place we need to conditionally render part of the UI.

import React, {useContext} from 'react';
import PermissionContext from "./PermissionContext";
import {Permission} from "../Types";

type Props = {
    to: Permission;
};

// This component is meant to be used everywhere a restriction based on user permission is needed
const Restricted: React.FunctionComponent<Props> = ({to, children}) => {

    // We "connect" to the provider thanks to the PermissionContext
    const {isAllowedTo} = useContext(PermissionContext);

    // If the user has that permission, render the children
    if(isAllowedTo(to)){
        return <>{children}</>;
    }

    // Otherwise, do not render anything
    return null;
};

export default Restricted;

Using these components, we can surround our application with the PermissionProvider, then use the Restricted component.

<PermissionProvider permissions={currentUser.permissions}>
   . . .
    <Restricted to="list.elements">
        <ElementList elements={elements} addElement={addElement} removeElement={removeElement}/>
    </Restricted>

   <div className="container">
       <table className="table table-sm table-hover">
           <thead className="thead-light">
           <tr>
               <th scope="col">Name</th>
               <th scope="col">Price</th>
               <th scope="col">Currency</th>
               <th scope="col" className="text-right">
                   <Restricted to='add.element'>
                       <button className="btn btn-primary btn-sm" onClick={addRandomElement}>
                           <i className="bi-plus-circle"/>
                       </button>
                   </Restricted>
               </th>
           </tr>
           </thead>
           <tbody>
           {elements.map(e => (
               <tr key={e.name}>
                   <td>{e.name}</td>
                   <td>{e.price}</td>
                   <td>{e.currency}</td>
                   <td className="text-right">
                       <Restricted to='delete.element'>
                           <button className="btn btn-danger btn-sm" onClick={() => removeElement(e)}>
                               <i className="bi bi-trash"/>
                           </button>
                       </Restricted>
                   </td>
               </tr>
           ))}
           </tbody>
       </table>
   </div>
</PermissionProvider>

This allows displaying the action button only to the users having the right permission.

Enhancements

Fallback renderer

In order to provide more flexibility to the developers and UI designers, it would be great to have the possibility to display an alternative UI in case the user does not have a particular permission. For this, a fallback property will be added to the Restriction component.

import React, {useContext} from 'react';
import PermissionContext from "./PermissionContext";
import {Permission} from "../Types";

type Props = {
    to: Permission;
    fallback?: JSX.Element | string;
};

// This component is meant to be used everywhere a restriction based on user permission is needed
const Restricted: React.FunctionComponent<Props> = ({to, fallback, children}) => {

    // We "connect" to the provider thanks to the PermissionContext
    const {isAllowedTo} = useContext(PermissionContext);

    // If the user has that permission, render the children
    if(isAllowedTo(to)){
        return <>{children}</>;
    }

    // Otherwise, render the fallback
    return <>{fallback}</>;
};

export default Restricted;

Custom hook

Creating a custom hook will allow the usage of the permission in more complex situations. This can be useful when we need to have a custom logic (not only rendering) based on the user’s permission.

import {useContext} from 'react';
import PermissionContext from "./PermissionContext";
import {Permission} from "../Types";

const usePermission = (permission: Permission) => {
    const {isAllowedTo} = useContext(PermissionContext);
    return isAllowedTo(permission);
}

export default usePermission;

This custom hook can now be used in the Restricted component.

import React from 'react';
import {Permission} from "../Types";
import usePermission from "./usePermission";

type Props = {
    to: Permission;
    fallback?: JSX.Element | string;
};

// This component is meant to be used everywhere a restriction based on user permission is needed
const Restricted: React.FunctionComponent<Props> = ({to, fallback, children}) => {

    // We "connect" to the provider thanks to the permission hook
    const allowed = usePermission(to);

    // If the user has that permission, render the children
    if(allowed){
        return <>{children}</>;
    }

    // Otherwise, render the fallback
    return <>{fallback}</>;
};

export default Restricted;

Going further

Now let’s consider a more complex (real-world) use case where the users have a lot of permissions and that those permissions cannot be fetched at login time, or the permissions can only be fetched by domain area.

In such cases, the permissions fetching and checking mechanism is asynchronous. This delay has to be taken into account at provider and at consumer levels.

Schema 2

In order to make it asynchronous, we need to update the provider to return a Promise from the isAllowedTo method. In the case of async permission fetching, the PermissionProvider now receives an “async method to get a permission” instead of a “list of permissions”.

At the same time, the requested permissions can be cached at PermissionProvider level in order to speed up the UI.

import React from 'react';
import {Permission} from "../Types";
import PermissionContext from "./PermissionContext";

type Props = {
    fetchPermission: (p: Permission) => Promise<boolean>
}

type PermissionCache = {
    [key:string]: boolean;
}

// This provider is intended to be surrounding the whole application.
// It should receive a method to fetch permissions as parameter
const PermissionProvider: React.FunctionComponent<Props> = ({fetchPermission, children}) => {

    const cache: PermissionCache = {};

    // Creates a method that returns whether the requested permission is granted to the current user
    const isAllowedTo = async (permission: Permission): Promise<boolean> => {
        if(Object.keys(cache).includes(permission)){
            return cache[permission];
        }
        const isAllowed = await fetchPermission(permission);
        cache[permission] = isAllowed;
        return isAllowed;
    };

    // This component will render its children wrapped around a PermissionContext's provider whose
    // value is set to the method defined above
    return <PermissionContext.Provider value=>{children}</PermissionContext.Provider>;
};

The custom hook gets updated

import {useContext, useState} from 'react';
import PermissionContext from "./PermissionContext";
import {Permission} from "../Types";

const usePermission = (permission: Permission) => {
    const [loading, setLoading] = useState(true);
    const [allowed, setAllowed] = useState<boolean>();

    const {isAllowedTo} = useContext(PermissionContext);

    isAllowedTo(permission).then((allowed) => {
        setLoading(false);
        setAllowed(allowed);
    })
    return [loading, allowed]
}

export default usePermission;

Finally, the consumer should await for the promise to complete. The question is “what should it display while the permission is fetched?”. The consumer can either render nothing or a loading component passed as parameter.

type Props = {
    to: Permission;
    fallback?: JSX.Element | string;
    loadingComponent?: JSX.Element | string;
};

// This component is meant to be used everywhere a restriction based on user permission is needed
const Restricted: React.FunctionComponent<Props> = ({to, fallback, loadingComponent, children}) => {

    // We "connect" to the provider thanks to the PermissionContext
    const [loading, allowed] = usePermission(to);

    if(loading){
        return <>{loadingComponent}</>;
    }

    // If the user has that permission, render the children
    if(allowed){
        return <>{children}</>;
    }

    // Otherwise, render the fallback
    return <>{fallback}</>;
};

That’s it folks

I really hope this article will help you design more user friendly user interfaces. You can find all the code here: https://github.com/francois-roget/permission-provider-demo (take a look at the tags for the different stages).

Written by

François Roget

Senior software engineer #Java #JavaScript #TypeScript #ReactJs