Building a Component Library from Scratch
How Oscar used React and CSS Modules to build a flexible component library for its many front-ends
Oscar’s technology suite is comprised of multiple applications. We build unique web-based interfaces for Oscar members that want to find and receive care, physicians that need to review their patients’ health history, and Care Guides that assist members with any health needs that arise. And while most of our users only ever log into one of these applications, there is enormous value in developing and using a single, powerful set of components that embody the Oscar brand.
To create a uniform experience, Oscar decided to develop a shared component library that unifies interactions and visuals across systems. By building a library accessible to Oscar front-end and mobile engineers across the organization, we also hoped to create a more consistent development environment for our tech team.
In Oscar’s earlier component libraries, we used a building-block approach by nesting well-defined and relatively simple components many times over. This approach allowed for a high degree of flexibility, and was easy to implement with React. Over time, however, this approach became verbose and lacked modularity. Larger components’ functionality depended on the correct usage and ordering of many components, as well as strict configuration for data being passed on properly. While this design didn’t directly affect users, it made development more tedious and slowed engineers unnecessarily.
Out of these frustrations, ideas for a new component library took form. The API for this shared component library is called Anatomy.
Oscar was already using React and CSS Modules in front-end development, so it was an easy decision to build Anatomy with the same technologies. React makes it simple to pass data from parent components to their children, while CSS Modules allow for strict modular styling.
Constrained flexibility was a major goal in the creation of Anatomy. There are many contexts and uses for a single component — i.e., a table that allows prospective members to compare plan options vs. a table that lists all claims for a given patient — yet, for consistency in design and code, the API and usage of these components needed to remain consistent.
After evaluating the shortcomings of the early versions of our component library, we decided to rethink the standard React way of building components. While most components work well with React’s standard API composition standards, components that are especially complicated — like highly customizable data tables and page layouts — are difficult to build. When rebuilding these more complex components, we wanted to satisfy the following:
- Encapsulation: Isolate internal implementation details from the external interface
- Granularity: Set reasonable defaults while providing a concise way to override behavior
- Ease of use: Remain within React API design conventions to allow for easy interoperability and developer productivity
We considered using React’s standard API, but its preferred structure of nesting components becomes complicated because the rules are strictly typed and hierarchical. With Anatomy’s configuration pattern, we’re able to derive the same benefits and final code structure of React’s standard API with a more developer-friendly API.
Anatomy: Example Use Cases of the Configuration Pattern
- Table Configuration
A table is the perfect example of why we needed to create Anatomy. In its most basic form, a table component should render a series of rows populated with data that gets passed in as a property. But at its most complex, a table can contain a configurable header, pagination, and intricate data display behavior. Nesting a lot of children to create these complex sub-components can cause issues with ordering and inter-component data sharing, as many of these sub-components will still need to access the raw data and reflect changes when that data becomes updated within another sub-component.
Complex nested configuration:
To solve for these issues, we created components used only for configuration. These configuration pseudo-components are passed into a root component that manages layout, determining which components to render based off the data provided. These pseudo-components are nested within the children of the root component, and have their own properties and children, allowing for more granular structures to be expressed in a single block of JSX. This both alleviates issues around nesting and ordering normal React rendering elements properly, and eliminates the need for a massive JSON object to pass in all properties.
JSON Table Configuration:
The implementation is simple: a dynamic component wraps and creates the actual component (in this case the Page Layout) that needs to be configured. The process is as follows:
- The dynamic components takes in a dictionary of component type names that map to property-type configurations corresponding to the components that need to be created
- These constructed React components only hold configuration options and are never rendered.
- The configuration wrapper higher-order component then takes these children configuration elements, and parses them into an elements object within the component. (For example, within the table component, accessing the Table.ColumnGroup configuration is as easy as calling this.elements.ColumnGroup.)
2. Page Layouts:
This same approach works well for page layouts. Responsive layouts have the potential to change markup ordering and direction based on page size, the sizes of its child components, and the requirements of its child components. It makes sense to wrap the page layout in a more aware component that takes in multiple JSX sections.
Oscar’s current implementation of this pattern does not allow for ordering or nesting of configuration components. This acts as a stopgap to prevent developers from building configurations that are too complex, breaking the core expectation of relatively simple component groups or effective nesting. The beauty of these limitations is that our implementation of the Configuration higher order component is less than two hundred lines of code, most of which is debugging warning code to allow for clear warnings and errors when developers pass in the wrong type to the component.
The Configuration API allows for granularity, and uses a similar approach to React’s API for nesting. Since implemented, we’ve rewritten many old library components to adopt this style of development. The Configuration Pattern API uses build-in React features, including property type validations, declaration types, and error checking. Many React developers are already familiar with these features, and can understand and develop components with this API very easily without learning an entirely new configuration pattern.
Oscar’s engineering organization has greatly benefited from Anatomy components’ strong configurability and consistency in core interactions and visual appearance. Look forward to a post on how we designed Anatomy in the coming months!