Go to content
The Codest
  • About Us
  • Services
  • Our Team
  • Case studies
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
  • About Us
  • Services
  • Our Team
  • Case studies
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
2023-05-19
Software Development

Enhancing Component Visibility in React with Conditional Rendering and Guards

Bartlomiej Kuczynski

Enhancing Component Visibility in React with Conditional Rendering and Guards - Image

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.

Task, problem solution, or what we would like to achieve

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 ifs to code, manually check conditions and show or not elements.

Let take a look to SimpleAppHeader component that contains few navigation elements.

export const SimpleAppHeader: FC = () => {
    return (
        <header>
            <nav>
                <ul>
                    {
                        currentUser.role === "ADMIN" &&
                        <li>Admin only component</li>
                    }
                    {
                        currentUser.role === "USER" &&
                        <li>User only component</li>
                    }
                    {
                        (currentUser.role === "ADMIN" || currentUser.role === "USER") &&
                        <li>Admin and User only component</li>
                    }
                    <li>General usage element</li>
                </ul>
            </nav>
        </header>
    )
}

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.

Simplification

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 (
        <header>
            <nav>
                <ul>
                    {
                        isAdmin(currentUser.role) &&
                        <li>Admin only component</li>
                    }
                    {
                        isUser(currentUser.role) &&
                        <li>User only component</li>
                    }
                    {
                        (isAdminOrUser(currentUser.role)) &&
                        <li>Admin and User only component</li>
                    }
                    <li>General usage element</li>
                </ul>
            </nav>
        </header>
    )
}

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.

Filtering

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 (
        <header>
            <nav>
                <ul>
                    {
                        hasRole(currentUser.role, ["ADMIN"]) &&
                        <li>Admin only component</li>
                    }
                    {
                        hasRole(currentUser.role, ["USER"]) &&
                        <li>User only component</li>
                    }
                    {
                        hasRole(currentUser.role, ["ADMIN", "USER"]) &&
                        <li>Admin and User only component</li>
                    }
                    <li>General usage element</li>
                </ul>
            </nav>
        </header>
    )
}

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.

The Guard Component

Time 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 :D

`GuardService`

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 (
        <header>
            <nav>
                <ul>
                    <Guard requiredRoles={["ADMIN"]} guardService={guardService}>
                        <li>Admin only component</li>
                    </Guard>
                    <Guard requiredRoles={["USER"]} guardService={guardService}>
                        <li>User only component</li>
                    </Guard>
                    <Guard requiredRoles={["USER", "ADMIN"]} guardService={guardService}>
                        <li>Admin and User component</li>
                    </Guard>
                    <li>General usage element</li>
                </ul>
            </nav>
        </header>
    );
}

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.

Forbidden element

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 (
        <header>
            <nav>
                <ul>
                    <Guard requiredRoles={["ADMIN"]} guardService={guardService}>
                        <li>Admin only component</li>
                    </Guard>
                    <Guard requiredRoles={["USER"]} guardService={guardService}>
                        <li>User only component</li>
                    </Guard>
                    <Guard requiredRoles={["MODERATOR"]} guardService={guardService}
                           forbidden={() => <div>Forbidden – only moderator can see this</div>}>
                        <li>Moderator component</li>
                    </Guard>
                    <Guard requiredRoles={["USER", "ADMIN"]} guardService={guardService}>
                        <li>Admin and User component</li>
                    </Guard>
                    <li>General usage element</li>
                </ul>
            </nav>
        </header>
    );
}

Flexibility of types

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<IRole> {
    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.

Specialized components and nesting

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<string>, "requiredRoles">) => {
    return <Guard<string> requiredRoles={["ADMIN"]} guardService={props.guardService} forbidden={props.forbidden}>
        {props.children}
    </Guard>
}

//…
export const SpecializedAppHeader: FC = () => {
    const guardService = new SimpleGuard();
    return (
        <header>
            <nav>
                <ul>
                    <AdminGuard guardService={guardService}>
                        <li>Admin only component</li>
                    </AdminGuard>
                    <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 case AdminGuard predefine ADMIN role. In consequences, we need to explicit define type of ROLE type parameter.

Sum up

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.

cooperation banner

Related articles

Software Development

Mobile App Data Collection: Security Concerns, Data Value, and Types of Data Collected by App Providers

In today's interconnected world, mobile apps have become an integral part of our daily lives. With the increasing dependence on these applications, the amount of data they collect, store, and process has become a critical concern.

Jakub Jakubowicz
Software Development

4 Common Web Accessibility Issues to Know

The web is used by millions of different people everyday, one of our main goals as developers is to make the web accessible for everyone. This article will introduce some common web accessibility issues and ways to solve them.

Reda Salmi
Software Development

Nuxt 3 - a Popular Hybrid Vue Framework

Nuxt 3 is the next generation of the popular hybrid Vue framework, which allows us to use Vue for building server-side rendered applications. Beta version was launched on 12 October 2021, bringing into Nuxt Vue 3, a new intro...

Filip Tobiasz
Software Development

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.

Cezary Goralski
Software Development

7 Tips and Tricks in React

Looking for ways to improve your React code? This article contains tips and tricks that every React Developer should know. Let's dive in!

Mohamed El Amine Daddou

Subscribe to our knowledge base and stay up to date on the expertise from industry.

About us

We are an agile software development company dedicated to empowering our clients' digital transformation projects and ensuring successful IT project delivery.

    United Kingdom - Headquarters

  • Office 303B, 182-184 High Street North E6 2JA London, England

    Poland - Local Tech Hubs

  • Business Link High5ive, Pawia 9, 31-154 Kraków, Poland
  • Brain Embassy, Konstruktorska 11, 02-673 Warsaw, Poland
  • Aleja Grunwaldzka 472B, 80-309 Gdańsk, Poland

    The Codest

  • Home
  • About us
  • Services
  • Case studies
  • Know how
  • Careers

    Services

  • PHP development
  • Java development
  • Python development
  • Ruby on Rails development
  • React Developers
  • Vue Developers
  • TypeScript Developers
  • DevOps
  • QA Engineers

    Resources

  • What are top CTOs and CIOs Challenges? [2022 updated]
  • Facts and Myths about Cooperating with External Software Development Partner
  • From the USA to Europe: Why do American startups decide to relocate to Europe
  • Privacy policy
  • Website terms of use

Copyright © 2022 by The Codest. All rights reserved.

We use cookies on the site for marketing, analytical and statistical purposes. By continuing to use, without changing your privacy settings, our site, you consent to the storage of cookies in your browser. You can always change the cookie settings in your browser. You can find more information in our Privacy Policy.