DEV Community

Cover image for Avoiding imperative if/else branching with composition in UI
David Sljukic
David Sljukic

Posted on

Avoiding imperative if/else branching with composition in UI

Hi,
In this post I'll be showing some small React code samples and updating them to look a bit nicer (IMO) by focusing on React composition. The samples are very similar to what I used for actual production features and does not require extra libraries apart from React.

AB testing

A/B testing is a way to compare two versions of something and through analytics figuring out which version performs better.

Let's say we need to show one version (variant A) of CTA (call to action) button to 50% of users who land on a page and 2nd version (variant B) to other 50%. This is being done so we can measure conversion rate in the app and determine which copy attracts more clicks on the CTA. The variant "a" or "b" is deterministically calculated based on user's data which is not relevant for the example. This means every time a user lands on the page, he will always see the same variant (same button in this case):

export const CTAButton = () => {
  const { variant } = useABExperiment('cta_button');

  if (variant === 'a') {
    return <Button>Learn more</Button>
  }

  {/* variant b testing the new button message */}
  return <Button>Schedule appointment today</Button>;
};
Enter fullscreen mode Exit fullscreen mode

Not too shabby.

Let's try a bit different approach:

export const CTAButton = () => (
  <ABExperiment name='cta_button'>
    <Variant name='a'>
      <Button>Learn more</Button>
    </Variant>

    <Variant name='b'>
      <Button>Schedule your online appointment today</Button>
    </Variant>
  </ABExperiment>
);
Enter fullscreen mode Exit fullscreen mode

It is not important to focus on the implementation of <ABExperiment> but more on the component composition. Focus on how a reviewer would feel to stumble upon this code during review vs a mixture of if, else, switch for each AB test in code.

By Removing if/else branching, these new components are used to declaratively define the AB test.

Having the useABExperiment hook, implementation would be straightforward:
<ABExperiment> is a context that wraps the useABExperiment hook that determines the variant 'a' or 'b' for that experiment based on user's data. <Variant> uses the <ABExperiment> context state and its name to determine whether to render or not. Bonus points if experiment and variant names are statically typed with TS.

After having 10 parallel AB experiments in a project each with its own if/else, if/return, switch... this seemed way more tidy.

Feature flags

Similar to the <ABExperiment /> API, it can be expanded to other places that use branching, like the feature flags:

const Plans = () => (
  <ul>
    <li><Button>Free</Button></li>
    <li><Button>Basic</Button></li>
    <li><Button>Premium</Button></li>
    <FeatureFlag name='premium-ultra'>
      <Flag name='on'>
        <li><Button>Premium Ultra</Button></li>
      </FlagOn>
      <Flag name='off'>
        <li><Button>Premium Plus</Button></li>
      </FlagOff>
    </FeatureFlag>
  </ul>
)
Enter fullscreen mode Exit fullscreen mode

Similar to ABExperiment, feature flags are remotely controlled and can be used to switch on/off features like the "premium-ultra plan feature" in this case.

Loading state

Loading state on the client side:

const SendMoneyForm = () => {
  // loading remote data with react-query (NOT RELEVANT)
  const { data, isLoading } = useCurrentUser()
  const [amount, setAmount] = React.useState(0)

  if (isLoading) return (
    <>
      <Skeleton size='md' />
      <Button disabled>Loding...</Button>
    </>
  )

  return (
    <>
      <CurrencySelector currency={data.currencies} />
      <AmountInput amount={amount} onChange={setAmount} />
      <Button>Send</Button>
    </>
  )
};
Enter fullscreen mode Exit fullscreen mode

Switching it up with LoadingGuard

const SendMoneyForm = () => {
  // loading remote data with react-query (NOT RELEVANT)
  const { data, isLoading } = useCurrentUser()
  const [amount, setAmount] = React.useState(0)

  return (
    <LoadingGuard
      isLoading={isLoading}
      skeleton={
        <>
          <Skeleton size='md' />
          <Button disabled>Loding...</Button>
        </>
      }>
      <CurrencySelector currencies={data.currencies} />
      <AmountInput amount={amount} onChange={setAmount} />
      <Button disabled={isLoading}>Send</Button>
    </LoadingGuard>
  )
Enter fullscreen mode Exit fullscreen mode

Example implementation of LoadingGuard:

type Props = {
  children: React.ReactNode;
  isLoading?: boolean;
  skeleton?: React.ReactNode;
};

export const LoadingGuard = ({
  children,
  isLoading,
  skeleton
}: Props) => {
  if (isLoading) return <>{skeleton}</>;

  return <>{children}</>;
};
Enter fullscreen mode Exit fullscreen mode

Platform switch

Let's say your product shares UX/UI (design-system components and/or front-end logic) between chrome extension and web app.

export const Settings = () => {
  const platform = usePlatform();

  return (
    <ul>
      <li><Button>Account settings</Button></li>
      <li><Button>Security settings</Button></li>
      <li><Button>Notification settings</Button></li>
      {/* show this option in chrome-extension only, not on web */}
      {platform === 'extension' && (
        <li><Button>Sync data</Button></li>
      )}
    </ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

Platform component:

export const Settings = () => (
  <ul>
    <li><Button>Account settings</Button></li>
    <li><Button>Security settings</Button></li>
    <li><Button>Notification settings</Button></li>

    {/* show this option in chrome-extension, not on web */}
    <Platform name='extension'>
      <li><Button>Sync data</Button></li>
    <Platform />
  </ul>
)
Enter fullscreen mode Exit fullscreen mode

Inspiration

  • React-router:
    <Router>
      <Route path='/x'>Yup</Route>
      <Route path='/y'>basically</Route>
      <Route path='/z'>same thing</Route>
    </Router>
Enter fullscreen mode Exit fullscreen mode

Final words

The ABExperiment, FeatureFlag, Platform components remind me of internal-domain-specific-languages.

Internal DSLs are particular ways of using a host language to give the host language the feel of a particular language.

I think using components like these makes code easier to review, because verifying branching logic in PRs can get very tedious.

Could you think of another branching component similar to those mentioned?

Let me know whether your project could benefit by using components in this matter.

Top comments (0)