Who are my children?

Harry Ledley
ActionIQ Tech Blog
Published in
5 min readSep 17, 2018

A deep dive into type-checking React children and the organizational ramifications of design consistency

At ActionIQ, we aim to simplify the complexity of customer data platforms (CDP) through our enterprise grade UIs. In the last few years as the CDP space has continued to grow and mature, so has our product. As our product expanded we aimed to build a coherent and consistent experience across our platform starting from the top with our <Layout /> all the way down to a simple <Button />. No matter where on our page you find a button, it should look and feel like an ActionIQ button.

This exploration led us to develop an internal Style Guide (see future blog post) that defines how components and elements at all levels should look and feel. The tricky question we began to ask ourselves as engineers was “How can we enforce certain relationships between elements?” If we have a <ButtonGroup />, how do we prevent engineers from accidentally filling it up with something besides a <Button />?

Flow solves this in a very clean way but we’re a TypeScript shop!* When we started researching this problem and looking for solutions we found that TypeScript did not, and still does not, support typing children (see the GitHub issue for more info). So as all engineers do, we decided to implement something ourselves.

Since all of our React components extend our internal<BaseComponent /> , adding validation logic to all of our components under the hood that needed it wasn’t so bad. We tried to explicitly define what type of children and how many children a component expected.

example childTypes from Layout.tsx, our generic Layout component

The example above defines our generic layout page structure. A page may (or may not) have a <Sidebar />, may (or may not) have a <Header />, but it must have exactly one <Body />. Our configuration structure, defined below, gets verified at runtime by our ChildValidator.

We have two valid “types”:

  1. Component a React class to expect as the child (may also use DOM elements by passing “div”, “a”, etc).
  2. (component: Component, props: IProps) => boolean a custom function to match on the provided component which also receives the parent’s props.

With each type you may define a count matcher:

  1. zeroOrOneOf
  2. zeroOrMoreOf
  3. oneOf
  4. oneOrMoreOf
  5. countOf
  6. countOrMoreOf
  7. countOrLessOf
  8. countBetweenOf

All matchers that may match on more than one component are greedy and will match all children possible until a mismatch.

Here are a few examples using various combinations of the above configuration:

React allows custom PropTypes functions so we were able to inject our ChildValidator into our BaseComponent’s children PropType. When the component validates its props we call our ChildValidator which iterates through this.props.children and verifies that the children passed match up against the types defined by childTypes.

As we defined more and more childTypes across our codebase we realized that we were constantly trying to then pull out specific children from this.props.children to tweak them according to the current state of the application. We generally made assumptions about the index of the child:

We realized we could extend our ChildValidator to return the valid children back to our component when requested.

Under the hood both the<Sidebar /> and the <Body /> care if the Sidebar is open or not and function differently depending on that. Instead of requiring the engineer to do this:

we allow the component to abstract away that logic for us and allow an engineer to only define the isOpen state once:

The power of this becomes clearer with something like a tabbing system

Now instead of needing to pass an isSelected value to each <Tab /> the wrapping <Tabs /> component can do that for us — reaching down into it’s own children for the value prop and comparing that it its own selected prop. Cloning elements and passing props isn’t unique to ChildValidator, but ChildValidator abstracts away a lot of the messiness of making sure you are cloning the right element.

The count matchers that match for a single object ( zeroOrOneOf and oneOf ) will return undefined | Component where as the remaining, which may match multiple, return an array containing all components matched. An empty array means nothing matched.

More components, more use cases, more improvements. We kept using this setup to proxy prop logic from one component to the next. Our render functions ended up growing larger and larger. We wanted to consolidate the cloning logic to a manageable place so that our render functions could be clean and concise. We expanded what you could do with childTypes

At runtime we match all children against Tab to verify that each child is an instance of Tab. At the same time we clone the element to inject the isSelected prop based on the match of the parent’s (Tabs) selected value with the child’s (Tab) own value. When calling this.matchChildren the mapped/cloned children are returned.

As of today we’ve been using our ChildValidator in production for close to a year. Although some new engineers feel restricted when using components that have childTypes defined, over time they’ve realized the benefits and how it enables us to create a consistent feel across our product while at the same time increasing engineering efficiency by preventing misuse of components.

We’ve had a similar conversations with UX — by restricting how engineers can use components we are in essence restricting the designs that UX can explore. The key point is that these restrictions come from UX. They have the power to restrict (or expand) how we use components by the way they visually and interactively design them. This forces UX to dig deep when determining those constraints. The more defined the constraints are up front the higher the velocity of both UX and engineering is when it comes to high fidelity mockups and implementing new pages. At the same time, the implementation cost of change goes down while the mental cost of change goes up. There is a forcing function to think about how an underlying component change affects all pages, not just a single use case.

There are still plenty of improvements to be made — add generic types to give more power to the results from matchChildren so you can make few assumptions in your render functions, remove all any uses from the library, support exact custom counts, and handle non-React children such as functions. You can find react-child-validator on NPM or GitHub. We are definitely open to both comments and contributions!

We’re very excited for the day when TypeScript supports validation at compile time but until then we’ll continue extending our ChildValidator to support new use cases as our Style Guide continues to grow.

* we spent a significant amount of energy debating Flow vs TypeScript in summer 2017. We will have a future blog post that goes into why we went with TypeScript.

--

--