Avoiding imperative if/else branching with composition in UI
Tags: react, frontend, typescript
Published: 2023-10-02 12:56:22

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):

```tsx
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>;
};
```

Not too shabby.

Let's try a bit different approach:

```tsx
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>
);
```

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:

```tsx
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>
)
```

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:

```tsx
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>
    </>
  )
};
```

Switching it up with `LoadingGuard`

```tsx
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>
  )
```

Example implementation of LoadingGuard:

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

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

  return <>{children}</>;
};
```

## Platform switch

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

```tsx
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>
  );
};
```

`Platform` component:

```tsx
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>
)
```

## Inspiration

- React-router:
  ```tsx
    <Router>
      <Route path='/x'>Yup</Route>
      <Route path='/y'>basically</Route>
      <Route path='/z'>same thing</Route>
    </Router>
  ```
- [Pattern matching in functional languages](https://youtu.be/Jt8iysvFvsY?si=92C3xXS16NtynJPS). Composition does not have anything to do with pattern matching, it's just that both deal with code branching in this case.

## Final words

The `ABExperiment`, `FeatureFlag`, `Platform` components remind me of [internal-domain-specific-languages](https://martinfowler.com/dsl.html).
> 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.