Javascript & React - How to avoid the "if hell" without using dictionary/objects?

113 Views Asked by At

I know this question has been asked before, but the usual answers bring another (potential) problem and doubts to my mind.

The context

I've got a function that returns what component has to be rendered depending on two parameters, paramA and paramB. The code, right now, looks something like this:

  if (paramA === paramATypes.PRE) {
    if (paramB === paramBTypes.REQUEST) {
      detailedView = (
        <ComponentA
          requestDetails={requestDetails as RequestDetailsDto<ComponentADto>}
        />
      );
    } else if (paramB === paramBTypes.MODIFICATION) {
      detailedView = (
        <ComponentB
          requestDetails={
            requestDetails as RequestDetailsDto<ComponentBDto>
          }
        />
      );
    }
  } else if (paramA === paramATypes.PRI) {
    if (paramB === paramBTypes.REQUEST) {
      detailedView = (
        <ComponentC
          requestDetails={requestDetails as RequestDetailsDto<ComponentCDto>}
        />
      );
    } else if (paramB === paramBTypes.MODIFICATION) {
      detailedView = (
        <ComponentD
          requestDetails={
            requestDetails as RequestDetailsDto<ComponentDDto>
          }
        />
      );
    } 
  } else if...

This goes on and on, as we have some different types of each, and each of them has a specific component that renders its properties in a certain way. Of course, it has been a bit oversimplified, just to be able to describe the situation.

The thing is, I could try to do something like this, as I usually do with simpler values:

const dict = {
  [paramATypes.PRE]: {
    [paramBTypes.REQUEST]: <ComponentA
      requestDetails={requestDetails as RequestDetailsDto<ComponentADto>}
    />,
    [paramBTypes.MODIFICATION]: <ComponentB
      requestDetails={
        requestDetails as RequestDetailsDto<ComponentBDto>
      }
    />,
  }, 
  ...
}

And then just call it like this:

const view = dict[paramA][paramB];

The problem

The problem I see with this is that with the "if-hell", the values of the components are only processed when the if conditions are met. So, in this case, it will only calculate/process one component per call.

However, if I use the object/dictionary paradigm, it will process all of the values because it needs it to create the actual object to be accessed later on, so each call to this method will calculate all of the possibilities, to just return one of them.

If the values were just declarations or simpler values, I wouldn't have any problem, but being React components I am not so sure.

The question

Am I right with the way it would be processed in both paradigms? What can I do to have a cleaner set of conditions?

Maybe wrap the values definition in a method, so it's only processed when I execute the result of the method, as in const view = dict[paramA][paramB]();?

I'm just wondering which would be the best way to put this so it's not only easier to read, but it also has a good performance (and good cognitive complexity in code analyzers).

Thank you!

3

There are 3 best solutions below

3
On BEST ANSWER

As I see it, there are 2 possible solutions:

  • the best, not always doable standardize all the props of the possible components to be rendered and create such a dictionary
const dict = {
  [paramATypes.PRE]: {
    [paramBTypes.REQUEST]: ComponentA,
    [paramBTypes.MODIFICATION]: ComponentB,
  },
} as const satisfies Record<
  paramATypes,
  Record<paramBTypes, React.FC<{ requestDetails: RequestDetailsDto }>>
>

And then in the component do something like this:

const Component = () => {
  const View = dict[paramA][paramB]

  return (
    <div>
      <View requestDetails={requestDetails} />
    </div>
  )
}

}
  • the most flexible, but less optimal
const Component = () => {
  const dict = {
    [paramATypes.PRE]: {
      [paramBTypes.REQUEST]: () => (
        <ComponentA
          requestDetails={requestDetails as RequestDetailsDto<ComponentADto>}
        />
      ),
      [paramBTypes.MODIFICATION]: () => (
        <ComponentB
          requestDetails={requestDetails as RequestDetailsDto<ComponentBDto>}
        />
      ),
    },
  } as const satisfies Record<paramATypes, Record<paramBTypes, React.FC>>

  const View = dict[paramA][paramB]

  return (
    <div>
      <View />
    </div>
  )
}


in both cases only the correct component is rendered and not all possible components, so performance-wise you are good

You should try the first option because it makes the props uniform and makes everything more maintainable,

2
On

I think the if statements are preferable to that other solution in terms of readability and simplicity. Other than that, you'd still have multiple ifs even if you had only one param since the components and their DTOs are unrelated to each other. So, the real problem lies elsewhere, imho. If they weren't you could use the Factory or Builder pattern to create the components. You can still use it as it is but it'd only "hide" that if chain of yours.

0
On

Just food for thought, because it is alot less commonly used but truly avoids if-else as well as dictionaries (where other solutions still use a dictionary): You could encode the if-else into a data structure and make it almost invisible:

/* This is the data structure that "stands in" for if-conditionals */
function Conditional(component, predicate) {
  // A "conditional component" is an object with two fields:
  // - The component, tacked into a thunk
  // - A variadic predicate function
  const it = Object.create(Conditional.prototype);
  it.component = () => component;
  it.predicate = predicate;
  return it;
}

Conditional.getRenderer = (...conditionals) => (...params) => {
  // Takes N conditional components and returns a renderer function that awaits data
  return conditionals.reduce(
    (result, conditional) => result == null ? conditional.render(...params) : result,
    null
  );
};

Conditional.prototype.render = function (...params) {
  // Renders a conditional component if the associated predicate accepts the data
  return this.predicate(...params) ? this.component() : null;
};


/* These are "dummy" components */
const ComponentA = (details) => `A(${details.msg})`;
const ComponentB = (details) => `B(${details.msg})`;
const ComponentC = (details) => `C(${details.msg})`;


/* The "render" function selects the right component */
const paramTypes = {
  A: 'a',
  B: 'b'
};

const render = Conditional.getRenderer(
  Conditional(ComponentA, (a, b) => a === paramTypes.A && b < 2),
  Conditional(ComponentB, (a) => a === paramTypes.B),
  Conditional(ComponentC, () => true)
);



/* Try it */
console.log(render('a', 0)({ msg: 'details' })); // Should be 'A(details)'
console.log(render('b')({ msg: 'details' }));    // Should be 'B(details)'
console.log(render()({ msg: 'details' }));       // Should be 'C(details)'

Note that there is a kind of "fallback" component involved (ComponentC)
which has a predicate associated that always returns true. When calling getRenderer this component must be the last one entered!

EDIT

Here's an example that uses React & ReactDOM with the same Conditional data structure just for demonstration purposes:

const h = React.createElement;
const $app = document.querySelector('#app');
const root = ReactDOM.createRoot($app);



// Conditional helper
function Conditional(component, predicate) {
  const it = Object.create(Conditional.prototype);
  it.component = () => component;
  it.predicate = predicate;
  return it;
}

Conditional.getRenderer = (...conditionals) => (...params) => {
  return conditionals.reduce(
    (result, conditional) => result == null ? conditional.render(...params) : result,
    null
  );
};

Conditional.prototype.render = function (...params) {
  return this.predicate(...params) ? this.component() : null;
};


// Program setup
const paramAType = {
  X: 'x',
  Y: 'y'
};

const paramBType = {
  A: 'a',
  B: 'b'
};


const Container = (...children) => {
  return h(
    'div',
    { className: 'components-container' },
    ...children
  );
};

const ComponentA = (props) => {
  return h(
    'div',
    { className: 'component component-a' },
    props.message
  );
};

const ComponentB = (props) => {
  return h(
    'div',
    { className: 'component component-b' },
    props.message
  );
};

const ComponentC = (props) => {
  return h(
    'div',
    { className: 'component component-c' },
    props.message
  );
};



const render = Conditional.getRenderer(
  Conditional(ComponentA, (paramA, paramB) => paramA === paramAType.X && paramB === paramBType.A),
  Conditional(ComponentB, (paramA, paramB) => paramA === paramAType.X && paramB === paramBType.B),
  Conditional(ComponentC, () => true)
);


root.render(
  Container(
    render(paramAType.X, paramBType.B)({ message: 'Component B!' }),
    render(paramAType.Y, paramBType.B)({ message: 'Component C!' }),
    render(paramAType.X, paramBType.A)({ message: 'Component A!' })
  )
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>