Copied to Clipboard
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={{isAllowedTo}}
>{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>
</PermissionProvider>
<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>
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
9
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 fetching and checking of the permission is asynchronous. This delay has to be taken into account at provider and at consumer levels.
Going further schema
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.
// 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={{isAllowedTo}}
>{children}</PermissionContext.Provider>;
};
At the same time, 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).