Pros and Cons of React
Why is it worth to use React? What advantages this JavaScript library has? To find out the answers dive into this article and discover the real benefits of using React.
Learn how to simplify and improve component visibility in React using conditional rendering and guard components.
Today I would like to discuss how to control component visibility in React. But before we start there is small
disclaimer:
Presented solution is not very secure in meaning of protecting your application against „hackers” (whoever there are).
Remember that you need to protect your endpoints and to use good practices in application design. This solution only
makes your work a little easier.
One of the most common functionalities is to show component only for a bunch of users that have some specific rights,
roles or privileges. Common solution is to add some if
s to code, manually check conditions and show or not elements.
Let take a look to SimpleAppHeader
component that contains few navigation elements.
<!-- wp:paragraph -->
<p><code>typescript jsx<br>
export const SimpleAppHeader: FC = () => {<br>
return (<br>
<header><br>
<nav><br>
<ul><br>
{<br>
currentUser.role === "ADMIN" &&<br>
<li>Admin only component</li><br>
}<br>
{<br>
currentUser.role === "USER" &&<br>
<li>User only component</li><br>
}<br>
{<br>
(currentUser.role === "ADMIN" || currentUser.role === "USER") &&<br>
<li>Admin and User only component</li><br>
}<br>
<li>General usage element</li><br>
</ul><br>
</nav><br>
</header><br>
)<br>
}<br>
</code></p>
<!-- /wp:paragraph -->
Looks „not bad”. Probably you could encapsulate checks into functions to reduce complexity. currentUser
is some object
that has role
property, but it could be anything that we would like to use as subject of our control mechanism.
This code has some problems. If project grows we probably use that constructions in many places. So we need to copy,
somehow, conditions. This code are hard to maintain and changing in a future. Specially when access rules change during
time e.g. we need to change currentUser
into something else. It is very hard to test. You need to write many tests
just to verify if your condition is ok.
Time to simplify this code a little. We could extract some methods and make code shorter and less complex:
const isAdmin = (role: string): boolean => role === "ADMIN";
const isUser = (role: string): boolean => role === "USER";
const isAdminOrUser = (role: string): boolean => isAdmin(role) || isUser(role);
export const SimplifiedAppHeader: FC = () => {
return (
{
isAdmin(currentUser.role) &&
Admin only component
}
{
isUser(currentUser.role) &&
User only component
}
{
(isAdminOrUser(currentUser.role)) &&
Admin and User only component
}
General usage element
)
}
```
Looks promising. We reduce noise od repeat lines. Code is more readable and easier to maintain. But take a look to
function isAdminOrUser
. We have only two roles. If we introduce third role we need to create another set of functions
that combines roles. Time to another version.
Let introduce function hasRole
that will be replacement for our isX
functions.
const hasRole = (role: string, requiredRole: string[]): boolean => {
let found: string | undefined = requiredRole.find(s => role === s);
return found !== undefined;
}
export const FilteringAppHeader: FC = () => {
return (
{
hasRole(currentUser.role, ["ADMIN"]) &&
Admin only component
}
{
hasRole(currentUser.role, ["USER"]) &&
User only component
}
{
hasRole(currentUser.role, ["ADMIN", "USER"]) &&
Admin and User only component
}
General usage element
)
}
```
Now looks good. We still have conditions in html part of code, but now we can test hasRole
function and trust that it
will be used with correct set of parameters. Adding or changing roles is now easier. We use array that contains all
roles that we need in place.
However, this solution is bind to currentUser
object. Assumption is that object is somehow global or easy to access in
any place in application. So we can try to encapsulate this. Of course, we can move it to hasRole
function:
const hasRole = (requiredRole: string[]): boolean => {
let found: string | undefined = requiredRole.find(s => currentUser.role === s);
return found !== undefined;
}
But that gives us almost nothing.
Guard
ComponentTime to create component that encapsulate whole logic. I named it Guard
and this is how I want to use it.
export const GuardedAppHeader: FC = () => {
return (
<header>
<nav>
<ul>
<Guard requiredRoles={["ADMIN"]}>
<li>Admin only component</li>
</Guard>
<Guard requiredRoles={["USER"]}>
<li>User only component</li>
</Guard>
<Guard requiredRoles={["USER", "ADMIN"]}>
<li>Admin and User component</li>
</Guard>
<li>General usage element</li>
</ul>
</nav>
</header>
);
}
Component need two properties. First children
responsible for content that is guarded. Second requiredRoles
that
handle array of roles that gives us access.
We need representation of this struct. It is very simple. We extends type React.PropsWithChildren
that has children
property. Of course you can manually add that property to your type and omit extension.
interface IGuardProps extends React.PropsWithChildren {
requiredRoles: string[];
}
Component itself is simple too. We will reuse hasRole
function here.
export const Guard = (props: IGuardProps) => {
const hasRole = (requiredRole: string[]): boolean => {
let found: string | undefined = requiredRole.find(s => currentUser.role === s);
return found !== undefined;
}
if (hasRole(props.requiredRoles)) {
return (
<>
{props.children}
</>
);
} else {
return <></>;
}
}
“`
And I could say stop here, but it will be too easy. Now we have component, and we can use it to the extreme 😀
First step will be externalization of checks. currentUser
is hardcoded value. I would like to encapsulate this value
into some service, that will „know” how to verify roles. Technically that means we move hasRole
function to another
class.
I create simple interface IGuardService
that has only one property – hasRole
.
export interface IGuardService {
checkRole: (roles: string[]) => boolean;
}
Now simple implementation could look like this
class SimpleGuard implements IGuardService {
checkRole(roles: string[]): boolean {
let found: string | undefined = roles.find(e => e === currentUser.role);
return found !== undefined;
}
}
To use it we need to change IGuardProperties
and use case.
export interface IGuardProps extends React.PropsWithChildren {
requiredRoles: string[];
guardService: IGuardService;
}
// …
const AppHeader: FC = () => {
const guardService = new SimpleGuard();
return (
Admin only component
User only component
Admin and User component
General usage element
);
}
```
Now component looks like:
export const Guard = (props: IGuardProps) => {
if (props.guardService.checkRole(props.requiredRoles)) {
return (
<>
{props.children}
</>
);
} else {
return <></>;
}
}
Much better. GuardService
separate us from logic that check roles. We can change it without consequences for our
component. Common use case is to use mock in tests and some „real” implementation in production code.
Next improvement will be handling custom Forbidden
element. Current solution renders empty element. First we need to
change IGuardProps
adding function that will be rendered that element.
export interface IGuardProps extends React.PropsWithChildren {
requiredRoles: string[];
guardService: IGuardService;
forbidden?: () => React.ReactNode;
}
This is optional property, name ends with question mark character. So it could be function or undefined
. We need to
handle it in Guard
component.
export const Guard = (props: IGuardProps) => {
if (props.guardService.checkRole(props.requiredRoles)) {
return (
<>
{props.children}
</>
);
} else if (props.forbidden === undefined) {
return <></>;
} else {
return (<>
{props.forbidden()}
</>
);
}
}
// …
export const AppHeader: FC = () => {
const guardService = new SimpleGuard();
return (
Admin only component
User only component
Forbidden – only moderator can see this
}>
Moderator component
Admin and User component
General usage element
);
}
```
Time to last big change. Current version component supports only roles as string
. We could have multiple different
types of property that we would like to check. Numbers or custom types is nothing special. I will add generics support.
First step is changes in IGuardService
interface. Implementations will be depended on type of tested value. It could
be anything, so interface should handle it.
export interface IGuardService<ROLE> {
checkRole: (roles: ROLE[]) => boolean;
}
Now it takes array of ROLE
generic type. Our simple implementation will change a little.
class SimpleGuard implements IGuardService<string> {
checkRole(roles: string[]): boolean {
let found: string | undefined = roles.find(e => e === currentUser.role);
return found !== undefined;
}
}
We need to add type parameter, but we could prepare implementation that supports IRole
interface.
interface IRole {
name: string;
}
//…
class RoleGuardService implements IGuardService {
checkRole(roles: IRole[]): boolean {
let found: IRole | undefined = roles.find(e => e === userWithRole.role);
return found !== undefined;
}
}
```
Second step is to propagate this change to IGuardProps
.
interface IGuardProps<ROLE> extends React.PropsWithChildren {
requiredRoles: ROLE[];
guardService: IGuardService<ROLE>;
forbidden?: () => React.ReactNode;
}
And respectively to Guard
component.
export const Guard = <S, >(props: IGuardProps<S>) => {
if (props.guardService.checkRole(props.requiredRoles)) {
return (
<>
{props.children}
</>
);
} else if (props.forbidden === undefined) {
return <></>;
} else {
return (<>
{props.forbidden()}
</>
);
}
}
And our use cases
export const AppHeader: FC = () => {
const guardService = new SimpleGuard();
const roleService = new RoleGuardService();
return (
<header>
<nav>
<ul>
<Guard<IRole> requiredRoles={[{name: "ADMIN"}]} guardService={roleService}>
<li>Admin only component</li>
</Guard>
<Guard<string> requiredRoles={["USER"]} guardService={guardService}>
<li>User only component</li>
</Guard>
<Guard<string> requiredRoles={["MODERATOR"]} guardService={guardService}
forbidden={() => <div>Forbidden – only moderator can see this</div>}>
<li>Moderator component</li>
</Guard>
<Guard<string> requiredRoles={["USER", "ADMIN"]} guardService={guardService}>
<li>Admin and User component</li>
</Guard>
<li>General usage element</li>
</ul>
</nav>
</header>
);
}
In this example I use both implementation of IGuardservice
just for illustrative purposes. In real use cases you
probably use only one.
The Guard
could be nested. Just remember that access will be resolved in order from most external instance.
export const NestAppHeader: FC = () => {
const guardService = new SimpleGuard();
return (
<header>
<nav>
<ul>
<Guard<string> requiredRoles={["USER", "ADMIN"]} guardService={guardService}>
<Guard<string> requiredRoles={["ADMIN"]} guardService={guardService}>
<li>Admin only component</li>
</Guard>
<Guard<string> requiredRoles={["USER"]} guardService={guardService}>
<li>User only component</li>
</Guard>
<li>Admin and User component</li>
<Guard<string> requiredRoles={["MODERATOR"]} guardService={guardService}
forbidden={() => <div>Forbidden – only moderator can see this</div>}>
<li>Moderator component</li>
</Guard>
</Guard>
<li>General usage element</li>
</ul>
</nav>
</header>
);
}
In above example Moderator component
could never appear, because user can handle only one role. First Guard
limits
roles to ADMIN
and USER
, so MODERATOR
will never pass first check.
We can build specialized components that hide some properties.
export const AdminGuard = (props: Omit<IGuardProps, "requiredRoles">) => {
return <Guard requiredRoles={["ADMIN"]} guardService={props.guardService} forbidden={props.forbidden}>
{props.children}
}
//…
export const SpecializedAppHeader: FC = () => {
const guardService = new SimpleGuard();
return (
Admin only component
<Guard requiredRoles={["USER"]} guardService={guardService}>
User only component
<Guard requiredRoles={["MODERATOR"]} guardService={guardService}
forbidden={() =>
Forbidden – only moderator can see this
}>
Moderator component
<Guard requiredRoles={["USER", "ADMIN"]} guardService={guardService}>
Admin and User component
General usage element
);
}
```
In this case AdminGuard
predefine ADMIN
role. In consequences, we need to explicit define type of ROLE
type
parameter.
In this article I show you how to create and use Guard
component in React. We start from complex code that is hard to
read and maintain. We evolve it into more developer friendly state and introduce custom functional component. Next we
extend component adding extra functionalities, refactor extracting service and finally add generics types.
Finally, we have have component that could be nested is easy to test and maintain.