DEV Community

loading...
Cover image for Conditionally render react components in cleaner way

Conditionally render react components in cleaner way

ms_yogii profile image Yogini Bende Updated on ・3 min read

Hello folks,

Almost in every app, we encounter situations where we need to render components or elements conditionally. If a user is logged in, the show user her profile else ask her to login, if the user is admin, show him admin pages etc. are just a few scenarios out of them. The most common practice for such use cases is use of if-else, ternary operators, && operators or switch cases.

Though these are simple to use options, at times, they may make your code messier. Today let’s cover two ways which are very effective and cleaner in terms of handling situations like this.

Using Enum

Let’s consider a scenario, where you want to display a settings page of your app based on the user role. The conditions here are,

  • If the user is admin, show her admin settings page.
  • If the user is not admin, show user settings page.
  • If the user is a guest, ask her to login to get her role.

Simple enough! Apart from this, let's also print the username on their respective pages. (This is just to understand how we can pass props to the components). I have created two different components, AdminSettings and UserSettings with some list items to replicate the real world component.

Both the component are as below -

const AdminSettings = ({ username }) => {
    return (
        <>
            <p>Hello {username}</p>
            <ul>
                <li>Admin Settings Option 1</li>
                <li>Admin Settings Option 2</li>
                <li>Admin Settings Option 3</li>
            </ul>
        </>
    );
};


const UserSettings = ({ username }) => {
    return (
        <>
            <p>Hello {username}</p>
            <ul>
                <li>User Settings Option 1</li>
                <li>User Settings Option 2</li>
                <li>User Settings Option 3</li>
            </ul>
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

Now let’s understand the conditional rendering. We will have one outer settings component, which will get both username and userRole. Using these two options we can decide which setting component to render. This outer settings component will have all the logic of this conditional rendering. Let’s first see the settings component and then understand the enum and conditional rendering.

const Settings = (props) => {
    const { userRole, username } = props;

    const roleSettings = (username) => ({
        admin: <AdminSettings username={username} />,
        user: <UserSettings username={username} />,
        guest: <p>Hello, you will need to login first!!</p>,
    });

    return (
        <div>
            <h1>Settings</h1>
            <p>{roleSettings(username)[userRole]}</p>
        </div>
    );
};

export default Settings;
Enter fullscreen mode Exit fullscreen mode

In the above code, the roleSettings function is considered as enum. Basically, it is just returning an object with different components. Hence in the return statement, we are actually trying to render one key of that object which matches the userRole. As that key contains the component, our required component will get rendered correctly.

As roleSettings is a function, the whole conditional rendering becomes very clean and easy to implement. Also, you don't need to hardcode many values in your application. You can pass props down to the components using the same function.

Apart from enum, other effective way is using HOC (Higher Order Component).

Using HOC

Higher Order Components in React are the wrapper components which takes the component as an argument and returns a component. Higher order components are considered very effective when working on role based access control systems. Though that is out of scope for this article, I will surely try to cover it in some of my next articles.

For now, just to give you a small example of HOC used for conditional rendering, lets consider the same use-case and use a HOC to show logged-in user.

The HOC will look like this -

function withLogin(Component) {
  return function EnhancedComponent({ isLoggedIn, ...props }) {
    if (isLoggedIn) {
      return <Component {...props} />;
    }

    return (
      <div>
        <p>Hello, please login to see your profile!</p>
      </div>
    );
  };
}

const ShowProfile = withLogin(Profile);

function App({ profile, isLoggedIn }) {
  return (
    <div>
      <h1>Hello Conditional Rendering</h1>

      <ShowProfile isLoggedIn={isLoggedIn} profile={profile} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

If you check the code, withLogin is a Higher Order Component, which will return detailed profile or settings page, if the user is logged in, else it will just return a message asking user to login.

We can use similar HOC for loaders or more complex permission based use-cases, which we will be covering in our next article.

If you are using more effective ways of conditional rendering or some different use-cases for HOCs, do share them with me in comments!

And your feedback on article will always be welcomed!!
Keep learning!

Discussion (33)

pic
Editor guide
Collapse
wintercounter profile image
Victor Vincent

For me HOCs in general are an anti-pattern, you can always find better solutions using hooks and context.

In this particular case showing the right component should be the role of your routing. For example using react-router you can simply do:

if (!loggedIn) {
    rerurn (
         <Redirect to={{
              path: '/login',
              state: { message: 'You need to login to see your settings` }
         }} />
     )
}
Enter fullscreen mode Exit fullscreen mode

Another anti-pattern is creating renderItem and such render functions. React components already functions itself. If you stick to it, you can have a much cleaner code base at the end.

The problem was also stated previously that in your examples all components will get constructed at load time for no reason. It was also stated this can be solved just by assigning the component itself to the property values instead the components with JSX definition, but that sacrifices dynamic usage, maintainability and edge case handling. This is why IMO switch-case is a superior solution.

To stick to your use case, I'd simply do the following:

const Settings = () => {
    const { role } = useContext(SessionContext)

    switch (role) {
        case 'admin': return <AdminSettings />
        case 'user': return <UserSettings />
        default: return <LoggedOut>Login to see your profile</LoggedOut>
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Follows a component-only pattern. No {renderSettings(role)}/{settingsComponents[role]}, but <Settings />
  • No HoC magic.
  • It doesn't bring up performances concerns.
  • More readable.
  • Better maintainability by supporting any other cases/props.
Collapse
lukeshiru profile image
▲ LUKE知る

If your concern is about defaults, you can always use the enum solution with the ?? operator:

const roleComponentMap = {
    admin: AdminProfile,
    user: UserProfile
};

const Profile = ({ role, ...props }) => {
    const Component = roleComponentMap[role] ?? DefaultProfile;

    return <Component {...props} />;
};
Enter fullscreen mode Exit fullscreen mode

or you can just use default values:

const roleComponentMap = {
    admin: AdminProfile,
    user: UserProfile,
    default: DefaultProfile
};

const Profile = ({ role = 'default', ...props }) => {
    const Component = roleComponentMap[role];

    return <Component {...props} />;
};
Enter fullscreen mode Exit fullscreen mode

In regards to "readability", and maintainability I could argue that this:

// Declare map
const somethingMap = {
  foo: Something,
  bar: SomethingElse
};
// Then use it
somethingMap[value]; 
Enter fullscreen mode Exit fullscreen mode

...feels better than this:

// declare function with a switch inside of it
const somethingMap = value => {
  switch (value) {
    case "foo": return Something;
    case "bar": return SomethingElse;
  }
};
// then use it
somethingMap(value);
Enter fullscreen mode Exit fullscreen mode
Collapse
wintercounter profile image
Victor Vincent

Default is not my concern at all, it can be replaced with case 'guest'. My problem is using an enum:

  • with component definitions (foo: <Bar />): it has performance costs
  • with direct component values (foo: Bar): it sacrifices flexibility and maintainability.

The last example you wrote is also an anti-pattern I explained.

As a hint, this code snippet alone here should be a red flag in the 90% of the cases: <Component {...props} />. By seeing the code you don't know what your component exactly is, and what props it is getting. This could be the equivalent of TS's any in React.

Thread Thread
lukeshiru profile image
▲ LUKE知る

Can you expand on the "cost" of flexibility and maintenance of component values? I mean if the props are the same for every component (which is expected if you're returning any of those out of the same function/component), then there is no issue with it.

As for the ...props, that depends on your setup. It might be a "red flag" in vanilla JS, but it isn't one if you have a type system. Here's an example with TS to illustrate:

import { FC } from "react";

type ProfileProps = JSX.IntrinsicElements["div"];
type ProfileComponent = FC<Props>;

const AdminProfile: ProfileComponent = props => <div {...props} />;
const UserProfile: ProfileComponent = props => <div {...props} />;
const DefaultProfile: ProfileComponent = props => <div {...props} />;

const roleComponentMap = {
  admin: AdminProfile,
  user: UserProfile,
  default: DefaultProfile
};

export const Profile: FC<ProfileProps & {
  readonly role?: keyof typeof roleComponentMap;
}> = ({ role = "default", ...props }) => {
  const Component = roleComponentMap[role]; // Component here is of type ProfileComponent 🎉

  return <Component {...props} />;
};
Enter fullscreen mode Exit fullscreen mode

Obviously this shouldn't be all on the same place, types in their own files, components as well ... but hey, is an example! ... Now, if you want to be extra safe because you don't trust TS, then you can change the Profile component a little:

export const Profile: FC<ProfileProps & {
  readonly role?: keyof typeof roleComponentMap;
}> = ({ role = "default", ...props }) => {
  const Component = roleComponentMap[role] ?? DefaultProfile;

  return <Component {...props} />;
};
Enter fullscreen mode Exit fullscreen mode
Thread Thread
wintercounter profile image
Victor Vincent

That it needs an extra tool (TS) to make it somewhat useable imo already validates my concern. You still cannot SEE from the code what it is.

Why is it so obvious that all components will receive the same prop always? Even in the example there is the logged out case which makes this invalid. Sure, it works fine until I need more/different props, but why would I settle down with a solution that doesn't let me modify later? Especially when the other solution doesn't cost me any extra effort over the enum one.

Thread Thread
lukeshiru profile image
▲ LUKE知る

Even in the example there is the logged out case which makes this invalid.

I used DefaultProfile instead, but ok...

import { FC } from "react";

type ProfileProps = JSX.IntrinsicElements["div"];
type ProfileComponent = FC<Props>;

const AdminProfile: ProfileComponent = props => <div {...props} />;
const UserProfile: ProfileComponent = props => <div {...props} />;
const LoggedOut: ProfileComponent = props => <div {...props}>Login to see your profile</div>;

const roleComponentMap = {
  admin: AdminProfile,
  user: UserProfile,
  default: LoggedOut
};

export const Profile: FC<ProfileProps & {
  readonly role?: keyof typeof roleComponentMap;
}> = ({ role = "default", ...props }) => {
  const Component = roleComponentMap[role];

  return <Component {...props} />;
};
Enter fullscreen mode Exit fullscreen mode

but why would I settle down with a solution that doesn't let me modify later?

I agree, solutions should take the future into consideration, but what in the code above makes you think you can't modify it later? Let's say now I want to add a new property "color" to Profile, that will set the classname of the rendered profile component to be color-${color}, with the above approach is quite easy:

export const Profile: FC<ProfileProps & {
  readonly role?: keyof typeof roleComponentMap;
  readonly color?: string;
}> = ({ className, color, role = "default", ...props }) => {
  const Component = roleComponentMap[role];

  return <Component className={color ? ` color-${color}` : undefined} {...props} />;
};
Enter fullscreen mode Exit fullscreen mode

With the switch/case, to achieve the same, I have to actually...

const Settings = ({ color }) => {
    const { role } = useContext(SessionContext)
    const className = color ? `color-${color}` : undefined;

    switch (role) {
        case 'admin': return <AdminSettings className={className} />
        case 'user': return <UserSettings className={className} />
        default: return <LoggedOut className={className}>Login to see your profile</LoggedOut>
    }
}
Enter fullscreen mode Exit fullscreen mode

The "initial" cost is pretty much the same, but the long term cost of the switch/case in this scenario is higher. When you're wrapping different components inside a single component (which is the case for this component), is better to share props across them because they will be used in the same places, so ideally they should receive the same props. They might ignore some of them, and use others.

Collapse
sultan99 profile image
Sultan

HOC is not an anti-pattern - it is HOF if we consider any component as a function, but hooks break the main FP rule - the function purity and I consider it as a side-effect. I agree with you, the best solution for this example are routers and HOC should be used for something else.

I prefer to create some helper functions which can be used across the project:

export const select = (...list) => value => list.reduce(
  (acc, next) => acc || next(value), null
)

export const when = (test, wanted) => value => (
  test === value && wanted
)
Enter fullscreen mode Exit fullscreen mode

and use them:

const DefaultSettings = () => (
  <p>What the Hell Are You?</p>
)

const selectSettings = select(
  when(`admin`, AdminSettings),
  when(`user`, UserSettings),
  when(`guest`, GuestSettings),
  () =>  DefaultSettings
)

const Settings = ({ userRole, username }) => {
  const Settings = selectSettings(userRole)
  return (
    <div>
      <h1>Settings</h1>
      <Settings username={username}/>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
Collapse
mikef profile image
Mike Feltman

"For me HOCs in general are an anti-pattern, you can always find better solutions using hooks and context."

Nailed it!

Collapse
jwp profile image
John Peters

Clever...

{roleSettings(username)[userRole]}
Enter fullscreen mode Exit fullscreen mode

Thanks!

Collapse
skyjur profile image
Ski

Clever is not a compliment in code and this example is not one that anyone should follow. Simple not clever is what we want.

Collapse
jwp profile image
John Peters

You mean function compositional patterns are ugly? I just stated it was clever because it took me a while to understand it.

Thread Thread
danielo515 profile image
Daniel Rodríguez Rivero

This is not about function composition, it is that you need to parse and execute the code in your head to understand what the outcome will be. Exactly the opposite as what declarative means.

Thread Thread
jwp profile image
John Peters

Be nice, I just got out of 3rd grade last year. My point is both functional composition and what was shown are similar with the confusion factor. The only difference is there is plenty of raving going out about the beauty of functional composition.

Thread Thread
danielo515 profile image
Daniel Rodríguez Rivero

I don't know where do you get that my answer was not nice, but I assure you I was not pretending to be aggressive.
I am a big fan of functional composition, but years and experience taught me that the code I want to debug is the simplest possible one. So I only use functional composition when it really improves simplicity or doesn't hurt maintainability. In this article, none of those are met.

Collapse
dungmidside profile image
Dung Tran

A downside of enum solution is all the component will be compiled even it doesn't need to rendered

Collapse
lukeshiru profile image
▲ LUKE知る

Not necessarily. It can be changed to look something like this:

import { createElement } from "react";

export const RoleSettings = {
  admin: AdminSettings,
  user: UserSettings,
  guest: GuestSettings,
};

export const Settings = ({ userRole, username }) => (
  <div>
    <h1>Settings</h1>
    <p>{createElement(RoleSettings[userRole], { username })}</p>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

or this:

export const roleSettings = {
  admin: AdminSettings,
  user: UserSettings,
  guest: GuestSettings,
};

export const Settings = ({ userRole, username }) => {
  const RoleSettings = roleSettings[userRole];
  return (
    <div>
      <h1>Settings</h1>
      <p>
        <RoleSettings username={username} />
      </p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In both snippets you make use of the "enum solution", and only create the instance of the component you're actually using :D

Collapse
ms_yogii profile image
Yogini Bende Author

Yeah. That's a good way to optimise when you are dealing with large components! We can modify the enum solutions more creatively to achieve both performance as well as simplicity of code.

Collapse
ms_yogii profile image
Yogini Bende Author

We can use enums more creatively and achieve performance as well. Luke have shown a really good example for that below.

Collapse
ridays2001 profile image
Riday

So, we have to compromise on performance to get clean code?

Collapse
ms_yogii profile image
Yogini Bende Author

Not really. Check what Luke have shared!

Thread Thread
ridays2001 profile image
Riday

Hmm... Makes sense... Nice article!

Collapse
georgesofianosgr profile image
George Sofianos

I've used HOCs before, but nowadays I'm trying to avoid them. I'm trying to keep my code small, simple, readable. This would be my solution.

const Settings = ({role, username}) => (
  {role === "admin" && <AdminSettings username={username} /> }
  {role === "user" && <UserSettings username={username} /> }
  {!role && <p>Hello, you will need to login first!!</p> }
)

const App = ({user}) => (
  <Settings role={user?.role} username={user?.username} />
)
Enter fullscreen mode Exit fullscreen mode
Collapse
krishan111 profile image
Krishan111

There are many small things in this post about which I even do not care and error comes XD, thanks for posting

Collapse
ms_yogii profile image
Yogini Bende Author

I hope this have helped you in some way!

Collapse
jennawagnercode profile image
Jenna Wagner

Yes that happens several time with me too

Collapse
evolify profile image
evolify

How about this ?

const visible = true
return(
    <div>
        <div r-if = {visible}>content</div>
    </div>
)
Enter fullscreen mode Exit fullscreen mode

see babel-plugin-react-directive

Collapse
thomasburleson profile image
Thomas Burleson

Enums are very powerful and restrict the keys that can be used.
For clarity, the roleSettings function returns a hashmap (key/value pairs). It is not an enum.

Thx for sharing two cool approaches to conditional renderings.
To validate the userRole and provide a fallback to guest, I recommend this:

Collapse
lxxmi3 profile image
Teabag

Great Post! Saved it for future implementations :D

Collapse
ms_yogii profile image
Yogini Bende Author

Thank you 🙌

Collapse
ben profile image
Ben Halpern

Great post

Collapse
jackmellis profile image
Jack

For many conditional rendering cases I really like jsx-control-statements (specifically If and Choose)
npmjs.com/package/babel-plugin-jsx...

Collapse
moezkouni profile image
MoezKouni

The best way is to use switch case statement to avoid constructing the other components. Otherwise, good article. Thanks

Collapse
kapekost profile image
Kostas Kapetanakis

Really great points!
A nice addition would be to add in the mix the Lazy loading, this way a user with role A won’t even have a link to receive bundles that won’t be meant for them ;)