Complex component APIs made simple

July 21, 2023

reactjavascriptcss

Firstly, simple is not easy.

In terms of word origins: Simple = Sim (One) + plex (fold)

"Simple" simply means without any entanglement.

On the other hand, "easy" means to be "near, at hand" - that is, "familiar".

When working on a big React project, we often make things by using various abstractions. A few of these are:

  • Prop drilling
  • Mixins (deprecated)
  • Factory pattern/Higher-order components
  • Render props
  • Children as function
  • Composition: using children + context/other global state

While this in itself is okay, some of these have more entanglement than others, and that can quickly complicate the understanding of "what's going on".

While doing some of these things may be easy, it is not simple.

For example, take this easy API for an accordion:

<Accordion items={[{
        title: 'Header 1',
        content: Header1Content
    }
    ...
]} />

Here, the whole mechanism is hidden away inside the accordion. The Accordion does a lot of things, which hides the complexity, but makes it pretty easy to use – just put in a JSON, and it just works!

But now, imagine if I had to close the accordion on clicking on something in the Header1Content. How would I do that?

Well, there are two ways

  1. Using refs
  2. Making the accordion uncontrolled

While using refs is okay, it just makes your code more "entangled" with each other.

On the other hand, if we use uncontrolled components, we would be "disentangling" the state-related concerns from the component.

So consider this:

// closed by default
const [openIndex, setOpenIndex] = useState();
return <Accordion openIndex={openIndex} setOpenIndex={setOpenIndex} items={{
    title: 'Header 1',
    content: () => <Header1Content onClose={() => setOpenIndex(0)}
}} />

In this way, we have extracted a state out and made it less entangled with the component, making it easier to change the behavior.

Now, this might look more "complex" to some of you, but it's just "not familiar" (and hence "not easy"). But this can be changed by making this kind of code "familiar" (and hence easier). Going by the pure meaning of it, we have made it simpler.

Now, let's go further.

Say, we want a different icon for each title.

Again, we have two options

  1. Pass an icon in the JSON object here & consume it.
  2. Convert the title to also accept a component (like content does).

Here, as before, option #1 will create more entanglement (and hence complexity). So option #2 seems like a winner.

But if you pay attention to it, even option #2 is going add a little complexity of its own - because we don't always want to pass a new icon for title, so we will want to support plain strings as well as components.

This kind of variable overloading will add more complexity inside the Accordion, which will need checks internally to see if we pass a string or a component. This in turn makes the usage complex, as we don't know what we can pass at a glance (Typescript can become hard to read too).

So, what do we do? Well, there's a third option, which is composition.

We can do this:

<Accordion>
    <Accordion.Item>
        <Accordion.Header>
        <Box>
            <BulbIcon />
            <Text>
                Header text
            </Text>
            </Icon>
        </Box>
        </Accordion.Header>
        <Accordion.Content>
            <Header1Content />
        </Accordion.Content>
    </Accordion.Item>
</Accordion>

This results in an extendible API that is simple (not entangled) and, over time, will get easy, as we get familiar with it.