Search code examples
reactjsdesign-patternsroles

A design pattern or architecture related to rendering components based on roles and permissions?


I want to know if there's a software design pattern that can solve a challenge I'm facing. I have a web application that works with several different user role types like SuperAdmin, Admin, Reseller, Dealer, Customer, Volunteer, etc...

And our React code is littered with if statements that check a person's role as well as other conditions to decide "what type of ui component" to present them. Here's a very simplified example:

// Container.js
<div>
   <h1>Something</h1>
   {['SuperAdmin', 'Admin'].includes(user.role) && (<input type="text" value="{record.first_name}" />)}
   {['Reseller', 'Dealer'].includes(user.role) && (<span className="border-2">{record.first_name}</span>)}
   {['Customer', 'Volunteer'].includes(user.role) && process.env.NETWORK_MODE === 'intranet' && (
    <div className="bg-grey">
        <span>Created at {record.create_at}</span>
        <span>This customer wants to be anonymous</span>
    </div>
   )}
   {['Reseller'].includes(user.role) && user.accountBalance > 0 && user.statementDate > (new Date()) && (
     <div>Please pay your bills</div>
   )}
</div>

The above example is very difficult for us to manage because there are over 100 components . When we need to change the access permissions for a role type or add other conditions, we have to go through every component and look for any stray if statements. We were thinking of writing some automated tests, but feel like we need to refactor the UI code before that's practical.

Are there some software engineering design patterns that would be relevant to my situation? If so, just state the names and i'l read up on it.


Right now, I'm contemplating an approach like this:

// Container.js
<div>
   <h1>Something</h1>
   <Customer user={user} />
   <BillNotice user={user} />
</div>

// Customer.js
const Customer = ({user}) => {
   if(['Admin', 'SuperAdmin'].includes(user.role)) return <CustomerVersion1 />;
   if(['Role', 'Role'].includes(user.role) && condition1 && condition2 ...etc...) return <CustomerVersion2 />;
   if(['Role', 'Role'].includes(user.role) && condition1 && condition2 ...etc...) return <CustomerVersion3 />;
   ...etc...
}

const CustomerVersion1 = () => <Component />;
const CustomerVersion2 = () => <Component />;
const CustomerVersion3 = () => <Component />;

...etc...

// BillNotice.js
const BillNotice = ({user}) => {
   if(['Admin', 'SuperAdmin'].includes(user.role)) return <BillNotice1 />;
   if(['Role', 'Role'].includes(user.role) && condition1 && condition2 ...etc...) return <BillNotice2 />;
   if(['Role', 'Role'].includes(user.role) && condition1 && condition2 ...etc...) return <BillNotice3 />;
   ...etc...
}

const BillNotice1 = () => <Component />;
const BillNotice2 = () => <Component />;
const BillNotice3 = () => <Component />;

Basically I'm making each component responsible for deciding how it should present itself. Then I can write unit test for each component by mocking the user argument and see if the appropriate component is returned. If this approach actually has a name, that would be great and I can read up more on it!


Solution

  • As far as my experience, there is no silver bullet for such an amount of conditions, after all, it exists and we need to implement them somewhere, however, we could write them only once (management in one place) if well designed IMO.

    perhaps using a hook is a good practice for this case.

    • ./hooks
    // control the logic in one place
    const useAuthControl = (userRole, conditions) => {
      // return as demand, maybe a more nested structure in the real world case
      return {
        isAdmin: ['Admin', 'SuperAdmin'].includes(userRole),
        isRole: ['Role', 'Role'].includes(userRole) && condition1 && condition2 ...etc...,
        isAnotherRole: ['Role', 'Role'].includes(userRole) && condition1 && condition2 ...etc...,
      }
    }
    
    • ./components
    // pass the condition dependencies if necessary
    const { isAdmin, isRole, isAnotherRole, ... } = useAuthControl(userRole, ...condition);
    
    // use for different components with a simpler method to write everywhere
    return (
      <>
        {isAdmin && ...}
        {isRole && ...}
        {isAnotehrRole && ...}
      </>
    )
    
    // other components as well
    return (
      <>
        {isAdmin && ...}
        {isRole && ...}
        {isAnotehrRole && ...}
      </>
    )