React Reusable Component Checklist

I have been working on several large React projects for a couple of Fortune 500 companies (with hundreds of developers) over the last two years, and inevitably some faction of the company publishes a library of “reusable” React components. While the intention is always good, the practical reusability of the components varies wildly. Sometimes the behavior of the pre-made component is not what I needed. Sometimes the component doesn’t follow accessibility or SEO guidelines. Most often the component is simply pre-styled and not customizable. (Being a CSS maverick, I can write complex selectors that dig down into the internal structure of the component and re-style it, but that is a fragile solution easily broken by minor changes to the component.) I have gathered these and other lessons into a checklist that I try to follow to ensure that my components are as reusable as possible. Without further ado, here is the checklist:


Accessible

  • color contrast ratio meets WCAG AA/AAA standards(this may be out-of-scope, as components should ship with only basic CSS layout — no theming)
  • keyboard-navigable
  • screen-reader-navigable (e.g. all buttons have text labels or aria-label, not just an icon)
  • semantic and minimal markup (e.g. don’t use ul for items that merely appear close to each other but are not otherwise connected)
  • use buttons for actions and anchors for navigating
  • use aria roles and attributes when needed
  • include a “skip links” anchor for navigation elements (see https://www.w3.org/TR/2015/WD-wai-aria-practices-1.1-20150514/#h-kbd_layout)
  • ensure that the component follows expected behavior as outlined by https://www.w3.org/TR/2015/WD-wai-aria-practices-1.1-20150514/ (e.g. modal components close on click of the escape key)

Localizable

  • accommodates LTR and RTL languages and layouts, perhaps via a standard prop like direction={‘ltr’|’rtl’}. For example, in a RTL environment, modal close buttons should be in the upper-left instead of upper-right, and primary content should be on the right instead of the left
  • gracefully handle long strings by wrapping or truncating appropriately
  • all text and iconography is injected via props, not handled internally. This allows the component to be localized lazily, at the last possible second before rendering. Avoid early injection and the inevitable Cambrian explosion of pre-localized components like <ModalEnglish /> and <ModalSpanish /> or <ModalRTL /> and <ModalLTR />

Customizable wrapper component tag

  • unless a semantically necessary tag (e.g. navigation) is needed, the wrapper should be <this.props.tagName which should default to a div or something generic
  • do not use a tag which requires a specific parent element, such as a tr or li. Doing this gives the consumer an unnecessary obligation

Pass along all unknown (unused) props via rest and spread

  • has {…props} on containing element. This allows refs, data attributes, and listeners to be attached without wrapping the component in an otherwise useless div. An excellent example of why to do this is when using Glamor, <SomeComponent {…myGlamorObj} />, it adds a data-css<hash> attribute/prop to the component. If SomeComponent doesn’t pass along unknown props, the final markup will not have the required data-css<hash> attribute
const { prop1, prop2, …props } = this.props;
return (
<this.props.tagName {…props} >
//...
</this.props.tagName>
);

Always accept className and style, usually apply to the component wrapper

  • Either pass explicitly (when needing to merge with internal props of the same name), or implicitly via {…props} (as above)

Ship naked

  • minimal CSS, mostly for layout of the component. Nothing is so frustrating as not being able to reuse a component just because it doesn’t look like what is needed and cannot be easily made to look like what is needed
  • do not inline CSS. This prevents specificity wars and makes it easier for the consumer to re-style
  • don’t force the consumer to know the structure of the component for styling purposes. Include props like buttonClass for nested elements of the component that consumers will likely wish to style (TODO: research/build custom pseudo-selectors for components)
  • for pre-themed components, ship naked by default and ship separate “themed” versions for quick consumption. Doing so will reveal pain points other consumers will run into while styling the component

Keep props minimal and flat

  • only ask for the specific values the component will use to render. Avoid requesting/depending on deeply-nested objects
  • keep abstractions to a minimum. It should be obvious what every prop is. That is, they should be well-named and/or easy to explain
  • avoid asking for everything and the kitchen sink. Don’t request props, especially global state props, that just get passed through. Instead, the children needing those props should be connected
  • use standard attribute names (like className instead of classes, or onCustomEvent instead of customEventCallback) to lower the learning curve and reduce surprise

Provide composable pieces, not a composition framework

  • to maintain similarity to React’s JSX declarative pattern, prefer
import { CreditCardIcon, CreditCardInput } from ‘/my/components’;
//...
return (
<form>
<CreditCardIcon cardType={this.state.cardType} />
<CreditCardInput onChange={this.setCardType} />
</form>
);
// OR
import { CreditCardIcon, CreditCardInput } from ‘/my/components’;
//...
return (
<form>
<CreditCardInput
icon={<CreditCardIcon cardType={this.state.cardType} />}
onChange={this.setCardType}
/>
</form>
);

to

import { CreditCardIcon, composeCCInput } from ‘/my/components’;
const CreditCardInput = composeCCInput({ icon: CreditCardIcon });
return (
<form>
<CreditCardInput />
</form>
);

because, in the latter example, the CreditCardIcon is inaccessible to the consumer. That is, the consumer cannot pass props directly to CreditCardIcon, drastically reducing its usefulness and the customizability of CreditCardInput


Higher Order Components

Accept a single parameter, a component, and return a component

  • by keeping to this rule, the HOC will be composable. For example,
// From https://github.com/acdlite/recompose
const enhance = compose(
withState(/*...args*/),
mapProps(/*...args*/),
pure
)
const EnhancedComponent = enhance(BaseComponent)
  • for HOCs that need to take configuration, partially apply the initial parameters and return an HOC that consumes a React component. For example configurableHOC(configuration)(someReactComponent)

Provide behavior, not markup

  • like Redux’sconnect, meet the expectation that passing a component to an HOC will add behavior, not return new markup “consuming” the component
One clap, two clap, three clap, forty?

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