Composing Reusable Components in React

This post is aimed at intermediate React users who may have implemented some application-level components, but don’t have a huge amount of experience with reusable, generalized components.

Credit to Ryan Florence, since much of this was either learned or honed in his Advanced React class; it was outstanding.

The goal

I’ll write a generalized accordion component highlighting React’s strength in managing state simply.

The accordion will accept a collection of accordion items, each specifying the caption to show in the toggle pane (what you click to expand or collapse it), and an optional defaultOpen property signifying initial state; support expand all, and collapse all; and allow developers to dynamically add or remove accordion panes.

And ideally, once this all works, we should let the user of the accordion assume complete control of whether any or all items are expanded or collapsed — in other words, support a “controlled mode” in React parlance.

Usage

Let’s start with the API — how do we want to use this Accordion?

Some AccordionItems are shown, some with defaults, others are added dynamically, some of which have defaults of their own.

We’ll start with a basic Accordion that supports most of the features we want, and iterate on that; but each implementation will run against that same code.

AccordionItem

The simplest part of this will be the AccordionItem. It will accept props of expanded, caption, onToggle, name, and when clicked slide the contents up or down based on whether it’s expanded. Its implementation is the simplest part of this, and looks like this

Of primary importance is that the AccordionItem is dumb. It receives an expanded prop, and passes it along to the Collapse component, from react-collapse. When the toggle link is clicked, it just calls the onToggle method which was passed in. It’s important, and difficult at first, to “think in React.” We don’t want communicating objects with state and behavior passing messages back and forth; we want a parent component passing updated props down the component tree, and for everything to “just work,” as a result.

So how can we actually implement our Accordion?

The simplest possible implementation

Let’s jump right in.

Those 30 lines produce a functioning accordion that looks like this — brace yourself, it’s completely un-styled; the point here is to focus on React fundamentals, not UX :)

Clicking the Pane links does slide the pane up and down, and the add and remove buttons also work.

So how does it work. The key is the openedHash state property of the Accordion. It stores which panes are open, and which are not.

In the constructor we iterate over the children of the accordion, and set up our hash, respecting the defaultOpen property of each AccordionItem, if any. And of course we need an if (child) { check since some of our items may be null. A more production-ready accordion might also inspect the type of each child, and for non AccordionItems, just pass them along. This would allow a user to render, say, an <hr/> between AccordionItems, or whatever; but we’ll keep it simple.

Then we define an onToggle method that takes a name in, and toggles the appropriate expanded state in our openedHash state.

Then, finally, in render we iterate through our children, filter out the nulls, and pass along the correct expanded state to the actual AccordionItems, as well as the onToggle method. It may seem odd at first cloning a react element in order to pass new props, but that’s how React does it.

Simple and humble.

What’s missing?

We don’t have expand/collapse all, our dynamically-added items do not have their defaultOpen values respected, and we have no way to let the user “control” each item’s state.

Expand/Collapse all

Let’s pick the low hanging fruit and implement expand and collapse all. If you’re thinking that you just need to add methods toggling each key in the openedHash appropriately, then good job; you’re thinking in React well.

We’ve added expandAll, and collapseAll methods which do what you’d expect. And we’ve also added buttons to expand all and collapse all; if you feel a little queasy about the inflexibility of these buttons, then stay tuned.

The layout is as ugly as it was, but it works.

Defaulting new items correctly

Next, let’s get those newly added items to have a correct initial state. The problem is that defaultOpen values are read initially, when the accordion is created, but not for newly added children.

For this, we’ll use the componentWillReceiveProps lifecycle hook, and check for any forthcoming children that we’re not yet aware of, and update our openedHash accordingly.

Note the code in componentWillReceiveProps which, again, just checks for new children. By now some duplication is starting to creep in. The initial defaultOpen setup is, when you think about it, the same as the work done in cWRP. And expandAll and collapseAll are really the same method, with a boolean arg separating them. We could consider refactoring, but let’s press on.

*EDIT* — after writing this it was pointed out to me that

let {openedHash} = this.state;

causes mutation directly against state, which will break PureComponents, or any component which relies on a shallow shouldComponentUdpate check. To be more correct that line should have been

let openedHash = {…this.state.openedHash};

Controlled mode

Let’s see what it would look like to let the user of the accordion dictate the state of each pane. The utility of this may very well be minimal in reality, but let’s see just how easy it is.

First, we do have to update the code that uses our accordion. We’ll now pass each AccordionItem its expanded property directly. We’ll also pass the accordion itself methods for onToggle, for when any accordion pane is toggled, as well as expandAll and collapseAll. This may seem inconvenient, but again, the whole point is that we want to take over controlling the state of each accordion pane (hence the name: “controlled mode”) so we naturally have to do the work.

Here’s the new usage code

The state for each accordion pane is now coming from the accordion’s parent’s state. It could also come from a Redux store, or MobX object if you’re using an external state manager.

Let’s see the updated Accordion, which now supports controlled mode.

It’s almost identical, except we’ve added an isControlled property that checks to see if the user passed in her own onToggle method — if so, we defer control. Our onToggle, expandAll, and collapseAll methods all defer to the versions passed in, if applicable.

The last piece is in the render method, when we clone the children, we now check for controlled mode, and if so, we pass along whatever expanded property the user has sent.

The changes were minimal; it’s that easy.

Improvement opportunities

Of course we could validate our props a bit better. But the more interesting problem is that our expandAll and collapseAll buttons are tightly coupled to the accordion, and all our AccordionItem details are hard coded and inflexible.

Let’s start with the accordion items. What if the user wanted to fade in and out the items? What if the user wanted to orient the accordion items from left to right, and just have the active ones slide down? What if the user had her own Collapse component she wanted to use?

In other words, what if the user just wanted our component to handle the state management of this accordion, but let her takeover the item rendering in toto?

Render callbacks handle this perfectly. I wrote a post about this very topic

https://medium.com/@adamrackis/simplifying-life-with-react-render-callbacks-cb37d58e55

but the gist is, rather than rendering content ourselves, we invoke a function with the props the user would need to render this content herself.

Let’s try it out.

Let’s have our AccordionItem component take a render prop, allowing the user to take over rendering, and customize the appearance, animation, etc.

If we get a render function, we just call it, and pass it what the user needs. Else we render the default content.

Using it is especially fun. We now have a blank slate. Our AccordionItems are giving us their state, and letting us render whatever we want, however we want.

Let’s give it a try. We’ll switch back to uncontrolled mode, for simplicity.

I’m rendering my own Fade component for the animations, and I’m adding my own styled anchor for the toggle. In reality we’d bottle that repeated markup into a dedicated component, rather than repeating it each time, but I wanted to keep it as simple to follow as possible.

And, inane though it may be, it works

And now the header

Let’s gradually modify our Accordion to give the user more and more control over where, and how the accordion header is rendered. To start, let’s get those header contents into a dedicated component, like this

The only change made is that the AccordionHeader contents are now in their own component. So far nothing’s better, but what if we export this header to our user, and let her render it, wherever she wants, like this. (the header is below pane A)

How can we get that to work? And ideally, how can we get that random span under the header to also render?

Before we were just naively assuming all children were accordion items, cloning them, and passing the props they’d need. But it turns out these children also have a type property, which we can check, and handle as needed.

First, we’re now using React’s Children.map, checking for null, and passing through when so; when the user sends us null children, we’ll just pass them along.

Then we check, in turn, for AccordionHeader and AccordionItem, and clone with the needed props. If the user sent us some other child, like the span, we just send it along in the final else. Now the user can intermix random contents amidst the accordion items and header. Sweet!

Ok, so the user has the privilege of putting the header wherever she wants. But do we really have to force her to? The header will normally go up top, so let’s default to that. How can we detect when the user has passed no header, and put one up front if so? Well we already have the type property, so the rest is just some basic JavaScript.

We’re having React convert children to a real array on line 5, then on lines 7–9 we check to see if an AccordionHeader is in the children anywhere, and if not, add one at index zero. Then a slight tweak in line 13 to just call the regular array map.

Can we customize the appearance of the header?

I think we already know the answer to that question :) If you’re thinking we should modify the AccordionHeader to accept a render callback, then you’ve clearly been paying attention!

Here’s what the finished product looks like; note the new AccordionHeader, and how it’s now used.

Which works.

I hope this post has helped some folks think in React. Remember, the goal is to compose small components together into something useful. If you ever find yourself needing to know the state of a child, you probably did something wrong. If you find yourself storing some of your children’s state, you probably did something wrong. If so, back up and see if there’s a simpler component model waiting to be had.

Comments or questions? Hit me up on Twitter at Adam Rackis since the commenting system here at Medium is horrible.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.