This page looks best with JavaScript enabled

Clean pattern for handling roles and permissions in large-scale React apps

 ·  ☕ 5 min read  ·  ✍️ Iskander Samatov

permissions in React



In this post, let’s cover how we can manage granular roles and permissions in React.

Any React application starts off nice and clean until you start layering conditional logic on top of it. And it quickly gets worse when you start adding granular permissions and roles.

Handling all those nuances is a nightmare when using simple conditionals or switch statements. There is a better way to do it.

Overview of the pattern

If you break it down, the pattern is simple. It involves building a gating wrapper and defining a set of maps with roles, scopes, and permissions.

The gating wrapper accepts an array of scopes the user must have to view protected components. We will also provide a couple of handy props for certain edge cases.

Tutorial

Gating Component

We will start by adding the gating component. Let’s call it PermissionsGate:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { cloneElement } from "react";
import { PERMISSIONS } from "./permission-maps";
import { useGetRole } from "./useGetRole";

const hasPermission = ({ permissions, scopes }) => {
  const scopesMap = {};
  scopes.forEach((scope) => {
    scopesMap[scope] = true;
  });

  return permissions.some((permission) => scopesMap[permission]);
};

export default function PermissionsGate({
  children,
  scopes = []
}) {
  const { role } = useGetRole();
  const permissions = PERMISSIONS[role];

  const permissionGranted = hasPermission({ permissions, scopes });

  if (!permissionGranted) return <></>

  return <>{children}</>;
}

PermssionsGate accepts children and scopes properties. We use scopes to declare which permissions the user must have to view the content of PermissionsGate.

useGetRole is a custom hook you must define that returns the current user’s role.

hasPermission returns a boolean of whether the user is granted access based on the scopes required and the user’s permissions.

Maps for roles and permissions

Now let’s define the scopes, user roles, and permissions maps we need. We’ll put them in separate permission-maps.js file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const ROLES = {
  viewer: "VIEWER",
  editor: "EDITOR",
  owner: "OWNER"
};

export const SCOPES = {
  canCreate: "can-create",
  canEdit: "can-edit",
  canDelete: "can-delete",
  canView: "can-view"
};

export const PERMISSIONS = {
  [ROLES.viewer]: [SCOPES.canView],
  [ROLES.editor]: [SCOPES.canView, SCOPES.canEdit],
  [ROLES.owner]: [
    SCOPES.canView,
    SCOPES.canEdit,
    SCOPES.canCreate,
    SCOPES.canDelete
  ]
};

We have three maps:

  • ROLES - All the user roles in our application.
  • SCOPES - Scopes we pass to our gating wrapper.
  • PERMISSIONS - The map that defines the set of scopes each user role possesses.

Now let’s see PermissionsGate in action. Let’s say the user must be an editor to view the page below:

private content
Private Content

All we need to do is wrap protected content with PermissionsGate and provide scopes necessary to view the content.

1
2
3
4
5
6
7
  <PermissionsGate
    scopes={[SCOPES.canEdit]}
  >
    <img alt="" className="vault-image" src={VAULT} />
    <h1>Private content</h1>
    <p>Must be an editor to view</p>
  </PermissionsGate>

The client API is clean, and it’s easy to tweak the scopes in the future if (when) business requirements change.

Additional use cases

So far, so good. But we can improve PermissionsGate further. Let’s adapt it to cover two more uses cases that often come up:

  • Display custom error message if the user doesn’t have correct permissions.
  • Pass a custom set of props down the component tree if the user doesn’t have correct permissions.

Let’s start with the first one. In some cases, we want to display a custom error component if the user doesn’t have the right permissions. We can do so by adding a prop to our PermissionsGate. Let’s call it RenderError:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export default function PermissionsGate({
  children,
  RenderError = () => <></>,
  scopes = []
}) {
  const { role } = useGetRole();
  const permissions = PERMISSIONS[role];

  const permissionGranted = hasPermission({ permissions, scopes });

  if (!permissionGranted && !errorProps) return <RenderError />;

  return <>{children}</>;
}

Let’s see it in action:

1
2
3
4
5
6
7
8
 <PermissionsGate
        RenderError={() => <p>You shall not pass!</p>}
        scopes={[SCOPES.canEdit]}
      >
        <img alt="" className="vault-image" src={VAULT} />
        <h1>Private content</h1>
        <p>Must be an editor to view</p>
  </PermissionsGate>

And here’s what we see now:

error message
Custom error message

Instead of rendering protected components, PermissionsGate renders our custom error message. That’s because we require canCreate scope to view the content and our user doesn’t have it.

Let’s move on to the second use case. Sometimes, you don’t necessarily want to hide children. Instead, you might want to make children aware by passing a custom set of props. One example could be disabling an input if a user is not an editor by providing a disabled property.

To pass custom props to children, we can use React.cloneElement :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
export default function PermissionsGate({
  children,
  RenderError = () => <></>,
  errorProps = null,
  scopes = []
}) {
  const { role } = useGetRole();
  const permissions = PERMISSIONS[role];

  const permissionGranted = hasPermission({ permissions, scopes });

  if (!permissionGranted && !errorProps) return <RenderError />;

  if (!permissionGranted && errorProps)
    return cloneElement(children, { ...errorProps });

  return <>{children}</>;
}

And here’s how you use it:

1
2
3
4
5
6
<PermissionsGate
  errorProps={{ disabled: true }}
  scopes={[SCOPES.canEdit]}
>
  <Input />
</PermissionsGate>

Our input will now be disabled for anybody without the SCOPE.canEdit scope.

Now our PermissionsGate is ready! By handling these two edge cases, we made sure we can take on any permission-related requirement that gets thrown at us.

Conclusion

With our gating component, we have a clean and scalable solution that lets us get as granular with our permissions as we need to.

Depending on your server and database setup, your implementation of gating component and permission maps may vary slightly. But the main principle stays the same.

As a quick reminder: never rely on checking permissions on the front-end application alone. Always remember to add additional permission validation on your server as well.

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on web and mobile development.