DEV Community

Emmanuel Sunday
Emmanuel Sunday

Posted on

Composition in React: Building like a Senior React Dev

Raise your hand if you've ever worked with an external UI library.

✋.

I'm a big fan of Shadcn myself.

These are the heroes front-end devs didn't ask for but need dearly.

Beautiful UIs. Quick Implementations.

Beautiful stuff.

But that's actually not the point of discussion.

Walk with me, let's do some analysis with these libraries.

React Composition

Assuming I wanted to work with a library like Shadcn, and chose to use the card components for whatever reason.

It's straightforward.

pnpm add shadcn@latest card
Enter fullscreen mode Exit fullscreen mode

Now, how do I use this card component?

export function Card() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <CardTitle>Login to your account</CardTitle>
        <CardDescription>
          Enter your email below to log in to your account
        </CardDescription>
        <CardAction>
          <Button variant="link">Sign Up</Button>
        </CardAction>
      </CardHeader>
      <CardContent>
        <form>
          <div className="flex flex-col gap-6">
            <div className="grid gap-2">
              <Label htmlFor="email">Email</Label>
              <Input
                id="email"
                type="email"
                placeholder="m@example.com"
                required
              />
            </div>
            <div className="grid gap-2">
              <div className="flex items-center">
                <Label htmlFor="password">Password</Label>
                <a
                  href="#"
                  className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
                >
                  Forgot your password?
                </a>
              </div>
              <Input id="password" type="password" required />
            </div>
          </div>
        </form>
      </CardContent>
      <CardFooter className="flex-col gap-2">
        <Button type="submit" className="w-full">
          Login
        </Button>
        <Button variant="outline" className="w-full">
          Login with Google
        </Button>
      </CardFooter>
    </Card>
  )
}
Enter fullscreen mode Exit fullscreen mode

This is just a sign-up page.

You know what?

I checked Chakra UI and loved their card implementation even more.

Watch this…

import { Avatar, Button, Card } from "@chakra-ui/react"

const Demo = () => {
  return (
    <Card.Root width="320px">
      <Card.Body gap="2">
        <Avatar.Root size="lg" shape="rounded">
          <Avatar.Image src="https://picsum.photos/200/300" />
          <Avatar.Fallback name="Nue Camp" />
        </Avatar.Root>
        <Card.Title mt="2">Nue Camp</Card.Title>
        <Card.Description>
          This is the card body. Lorem ipsum dolor sit amet, consectetur
          adipiscing elit. Curabitur nec odio vel dui euismod fermentum.
          Curabitur nec odio vel dui euismod fermentum.
        </Card.Description>
      </Card.Body>
      <Card.Footer justifyContent="flex-end">
        <Button variant="outline">View</Button>
        <Button>Join</Button>
      </Card.Footer>
    </Card.Root>
  )
}
Enter fullscreen mode Exit fullscreen mode

What did you notice common in these demos?

They have a parent component and child components that somehow come together to make things work.

….and voila…you have a card component.

Chakra UI even goes a step further to do something weird.

<Card.Root>
  <Card.Description>
  </Card.Description>
</Card.Root>
Enter fullscreen mode Exit fullscreen mode

Can JSX now do dot notations?!!

How do all these work?

This whole thing is known as composition.

In React terms, it's called compound components.

This is how you build UIs for scalability, reusability, and separation of concerns.

And that's why these big UI libraries employ that.

Now, why do you use them? How do you use them? When should you use them? I'm answering all these questions in a moment.

The big question: why?

I applied to a frontend role earlier this week and was taken to the next stage of assessment.

We were told to build a checkout page and were presented with the UI on Figma.

Long story short, this is what it looks like…

Very simple.

It's also live. You can check it out: https://crypto-checkout-omega.vercel.app/

Now, how do you build a checkout like this, thinking in systems?

Focus on this word…

Utility.

Inversion of Control

I'll digress a bit to talk about building a simple onboarding page.

Very simple.

if (step === 1) { ... }
else if (step === 2 && isBusiness) { ... }
else if (step === 2 && !isBusiness) { ... }
else if (step === 3 && country === 'NG') { ... }
Enter fullscreen mode Exit fullscreen mode

Right?

To be fair, this is a good implementation.

...but very far from an enterprise-grade solution.

It struggles with maintainability, scalability, and reusability.

It actually looks a lot better in my illustration.

I've seen situations where this gets to 700 lines of code with so many conditions, and it becomes difficult to wrap your head around it.

Do you want to know what a Composition (Compound Components) implementation for this looks like?

<Onboarding>
  <Onboarding.Step id="account">
    <AccountSetup />
  </Onboarding.Step>

  <Onboarding.Step id="kyc">
    <KYCForm />
  </Onboarding.Step>

  <Onboarding.Step
    id="business"
    when={userType === 'business'}
  >
    <BusinessDetails />
  </Onboarding.Step>

  <Onboarding.Step
    id="bank"
    when={country === 'NG'}
  >
    <BankAccount />
  </Onboarding.Step>

  <Onboarding.Complete />
</Onboarding>
Enter fullscreen mode Exit fullscreen mode

And that's basically every code.

Composition is a design pattern in which multiple components are designed to work together by sharing implicit state and behavior.

It works with something called "Inversion of control."

Inversion of Control is a design principle where the flow of control is delegated to a higher-level component, rather than being explicitly managed by the consumer.

In simpler terms:

  • Traditional control: You call the code
  • Inverted control: The code calls you

Here's what I mean…

Traditional Control…

<Checkout
  amount={5000}
  paymentMethod="card"
  onPaymentMethodChange={setMethod}
/>
Enter fullscreen mode Exit fullscreen mode

Inverted Control…

<Checkout>
  <Checkout.Amount />
  <Checkout.PaymentMethods />
  <Checkout.Submit />
</Checkout>
Enter fullscreen mode Exit fullscreen mode

The component "Checkout" basically handles state and logic, while you control the layout by how many "child components" you choose to put out.

If, for instance, I now want to have a forward feature, I only throw in a <Checkout.Forward /> component to handle that.

<Checkout>
  <Checkout.Amount />
  <Checkout.PaymentMethods />
  <Checkout.Submit />
  <Checkout.Forward />
</Checkout>
Enter fullscreen mode Exit fullscreen mode

Say you're building a checkout page that has limited features for certain countries due to restrictions. This becomes a beauty to handle.

Utility driven

Composition becomes very useful when you're building single components that aim to be "utilitarian".

E.g., Imagine building a Card UI like Chakra UI does.

So you write

export function Card({
  title,
  subtitle,
  description,
  imageUrl,
  imageAlt,
  showHeader = true,
  showFooter = true,
  primaryActionLabel,
  onPrimaryActionClick,
  secondaryActionLabel,
  onSecondaryActionClick,
  footerText,
  isLoading,
  disabled,
  theme = 'light',
}: CardProps) {
  return (
    <div className={`card card--${theme}`}>
Enter fullscreen mode Exit fullscreen mode

Hectic!

You'll have so many moving parts (that would never be enough) sent as props. So many things to remember. And yet not with much control.

At this point, Composition saves the day.

<Card theme="dark">
  <Card.Header>
    <h3>Premium Plan</h3>
    <p>Best for teams</p>
  </Card.Header>

  <Card.Image src="/plan.png" alt="Plan image" />

  <Card.Body>
    <p>Unlimited projects and advanced analytics</p>
  </Card.Body>

  <Card.Actions>
    <button>Subscribe</button>
    <button>Learn more</button>
    <small>Cancel anytime</small>
  </Card.Actions>
</Card>
Enter fullscreen mode Exit fullscreen mode

Voila, that's all you need!

Implementing Compound Components

Now, let's go back to our onboarding illustration.

<Onboarding>
  <Onboarding.Step id="account">
    <AccountSetup />
  </Onboarding.Step>

  <Onboarding.Step id="kyc">
    <KYCForm />
  </Onboarding.Step>

  <Onboarding.Step
    id="business"
    when={userType === 'business'}
  >
    <BusinessDetails />
  </Onboarding.Step>

  <Onboarding.Step
    id="bank"
    when={country === 'NG'}
  >
    <BankAccount />
  </Onboarding.Step>

  <Onboarding.Complete />
</Onboarding>
Enter fullscreen mode Exit fullscreen mode

Here's how we can implement this…

Onboarding.tsx

const OnboardingContext = React.createContext<any>(null);

export const Onboarding = ({ children }: { children: React.ReactNode }) => {
  const [currentStep, setCurrentStep] = React.useState(0);

  const steps = React.Children.toArray(children).filter(
    (child: any) => child.props.when !== false
  );

  const value = {
    currentStep,
    totalSteps: steps.length,
    next: () => setCurrentStep((s) => s + 1),
    prev: () => setCurrentStep((s) => s - 1),
  };

  return (
    <OnboardingContext.Provider value={value}>
      {steps[currentStep]}
    </OnboardingContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Step Component

Onboarding.Step = ({ children }: { children: React.ReactNode }) => {
  return <>{children}</>;
};
Enter fullscreen mode Exit fullscreen mode

Complete Component

Onboarding.Complete = () => {
  return <div>🎉 Onboarding complete!</div>;
};
Enter fullscreen mode Exit fullscreen mode

Final usage…

<Checkout>
  <Checkout.MethodTabs />

  <Checkout.When value="card">
    <CardPayment />
  </Checkout.When>

  <Checkout.When value="bank">
    <BankTransfer />
  </Checkout.When>

  <Checkout.When value="crypto">
    <CryptoPayment />
  </Checkout.When>

  <Checkout.Action />
</Checkout>
Enter fullscreen mode Exit fullscreen mode

The only complex logic we had going on here is…

const steps = React.Children.toArray(children).filter(
  (child: any) => child.props.when !== false
);
Enter fullscreen mode Exit fullscreen mode

And it's basically a method that turns React children into arrays.

child.props.when !== false then goes on to filter the individual child so when the "when" prop they receive is false, they don't get to be rendered.

You also need the knowledge of React Context API to understand what was going on there.

Back to my Interview

This is what my final parent "Checkout" component looked like

<Checkout>
  <Checkout.TransferTabs />

  <Checkout.Body>
    <Checkout.CryptoToCash />
    <Checkout.CashToCrypto />
    <Checkout.ComingSoon />
  </Checkout.Body>

  <Checkout.PrimaryAction />
</Checkout>
Enter fullscreen mode Exit fullscreen mode

So how did I implement this?

Think deeply (maybe ask ChatGPT 😂).

Don't Overengineer

If it's a utilitarian component. Engineer. Not a utilitarian component. Do not engineer.

E.g., I still comfortably had this somewhere in my code…

<AmountInput 
  label='You pay'
  value={payingAmount}
  onChange={handlePayingAmountChange}
  placeholder="0.00"
  selectedCrypto={selectedPayingCrypto}
  onCryptoChange={setSelectedPayingCrypto}
/>
Enter fullscreen mode Exit fullscreen mode

Unless I'm also going to build a calculator and so badly want to cling to the logic I had with it (which is beautiful, btw), there's no need to make "AmountInput" a compound component.

It has only one purpose to serve me, and not many moving parts.

Love and light. Peace ✌️

Top comments (1)

Collapse
 
praise_chidinma_106de4639 profile image
Praise Chidinma

This is very informative 💯