loading...
Cover image for The 10 Component Commandments

The 10 Component Commandments

selbekk profile image selbekk Originally published at selbekk.io Updated on ・11 min read

Written in collaboration with Caroline Odden. Based on the talk with the same name and people, held at the ReactJS Oslo Meetup in June 2019.

Creating components that are used by a lot of people is hard. You have to think pretty carefully about what props you should accept, if those props are supposed to be part of a public API.

This article will give you a quick introduction to some best practices within API design in general, as well as the definite list of 10 practical commandments you can use to create components that your fellow developers will love to use.

An illustration of a person thinking about APIs

What's an API?

An API - or Application Programming Interface - is basically where two pieces of code meet. It's the contact surface between your code and the rest of the world. We call this contact surface an interface. It's a defined set of actions or data points you can interact with.

The interface between your backend and your frontend is an API. You can access a given set of data and functionality by interacting with this API.

The interface between a class and the code calling that class is an API, too. You can call methods on the class, to retrieve data or trigger functionality encapsulated within it.

Following the same train of thought, the props your component accept is also its API. It's the way your users interact with your component, and a lot of the same rules and considerations applies when you decide what to expose.

Some best practices in API design

So what rules and considerations apply when designing an API? Well, we did a bit of research on that end, and turns out there's a lot of great resources out there. We picked out two - Josh Tauberer's "What Makes a Good API?" and Ron Kurir's article with the same title - and we came up with 4 best practices to follow.

Stable versioning

One of the most important things to consider when you're creating an API, is to keep it as stable as possible. That means minimizing the amount of breaking changes over time. If you do have breaking changes, make sure to write extensive upgrade guides, and if possible, provide a code-mod that automates that process for the consumer.

If you're publishing your API, make sure to adhere to Semantic Versioning. This makes it easy for the consumer to decide what version is required.

Descriptive error messages

Whenever an error occurs when calling your API, you should do your best to explain what went wrong, and how to fix it. Shaming the consumer with a "wrong usage" response without any other context doesn't seem like a great user experience.

Instead, write descriptive errors that help the user fix how they call your API.

Minimize developer surprise

Developers are flimsy beings, and you don't want to startle them when they are using your API. In other words - make your API as intuitive as possible. You can achieve that by following best practices and existing naming conventions.

Another thing to keep in mind is being consistent with your code. If you're prepending boolean property names with is or has one place, and skip it the next - that's going to be confusing to people.

Minimize your API surface

While we're speaking of minimizing stuff - minimize your API as well. Tons of features are all well and good, but the less surface your API has, the less your consumers will have to learn. That - in turn - is perceived as an easy API to use!

There are always ways to control the size of your APIs - one is to refactor out a new API from your old one.

The 10 Component Commandments

An illustration depicting the JSX "<Commandments />"

So these 4 golden rules work well for REST APIs and old procedural stuff in Pascal - but how do they translate to the modern world of React?

Well, as we mentioned earlier, components have their own APIs. We call them props, and it's how we feed our components with data, callbacks and other functionality. How do we structure this props object is such a way that we don't violate any of the rules above? How do we write our components in such a way that they're easy to work with for the next developer testing them out?

We've created this list of 10 good rules to follow when you're creating your components, and we hope you find them useful.

1. Document the usage

If you don't document how your component is supposed to be used, it's by definition useless. Well, almost - the consumer could always check out the implementation, but that's rarely the best user experience.

There are several ways to document components, but in our view there are 3 options that we want to recommend:

The first two give you a playground to work with while developing your components, while the third one let's you write more free-form documentation with MDX.

No matter what you choose - make sure to document both the API, as well as how and when your component is supposed to be used. That last part is crucial in shared component libraries - so people use the right button or layout grid in a given context.

2. Allow for contextual semantics

HTML is a language for structuring information in a semantic way. Yet - most of our components are made out of <div /> tags. It makes sense in a way - because generic components can't really assume whether it's supposed to be an <article /> or <section /> or an <aside /> - but it isn't ideal.

Instead, we suggest that you allow your components to accept an as prop, which will consistently let you override what DOM element is being rendered. Here's an example of how you could implement it:

function Grid({ as: Element, ...props }) {
  return <Element className="grid" {...props} />
}
Grid.defaultProps = {
  as: 'div',
};

We rename the as prop to a local variable Element, and use that in our JSX. We give a generic default value for when you don't really have a more semantic HTML tag to pass.

When time comes to use this <Grid /> component, you could just pass the correct tag:

function App() {
  return (
    <Grid as="main">
      <MoreContent />
    </Grid>
  );
}

Note that this will work just as well with React components. A great example here is if you want to have a <Button /> component render a React Router <Link /> instead:

<Button as={Link} to="/profile">
  Go to Profile
</Button>

3. Avoid boolean props

Boolean props sound like a great idea. You can specify them without a value, so they look really elegant:

<Button large>BUY NOW!</Button>

But even if they look pretty, boolean properties only allow for two possibilities. On or off. Visible or hidden. 1 or 0.

Whenever you start introducing boolean properties for stuff like size, variants, colors or anything that might be anything other than a binary choice down the line, you're in trouble.

<Button large small primary disabled secondary>
  WHAT AM I??
</Button>

In other words, boolean properties often doesn't scale with changing requirements. Instead - try to use enumerated values like strings for values that might have a chance to become anything other than a binary choice.

<Button variant="primary" size="large">
  I am primarily a large button
</Button>

That's not to say that boolean properties doesn't have a place. They sure do! The disabled prop I listed above should still be a boolean - because there is no middle state between enabled and disabled. Just save them for the truly binary choices.

4. Use props.children

React has a few special properties that are dealt with in a different way than the others. One is key, which are required for tracking the order of list items, and another one is children.

Anything you put between an opening and a closing component tag is placed inside the props.children prop. And you should use that as often as you can.

The reason for this is that it's much easier to use than having a content prop or something else that typically only accepts a simple value like text.

<TableCell content="Some text" />

// vs

<TableCell>Some text</TableCell>

There are several upsides to using props.children. First of all, it resembles how regular HTML works. Second, you're free to pass in whatever you want! Instead of adding leftIcon and rightIcon props to your component - just pass them in as a part of the props.children prop:

<TableCell>
  <ImportantIcon /> Some text
</TableCell>

You could always argue that your component should only be allowed to render regular text, and in some cases that might be true. At least for now. By using props.children instead, you're future proofing your API for these changing requirements.

5. Let the parent hook into internal logic

Some times we create components with a lot of internal logic and state - like auto-complete dropdowns or interactive charts.

These types of components are the ones that most often suffer from verbose APIs, and one of the reasons is the amount of overrides and special usage you usually have to support as time goes by.

What if we could just provide a single, standardized prop that could let the consumer control, react to or plain override the default behavior of your component?

Kent C. Dodds wrote a great article on this concept called "state reducers". There's a post about the concept itself, and another one on how to implement it for React hooks.

Quickly summarized, this pattern of passing in a "state reducer" function to your component will let the consumer access all the actions dispatched inside of your component. You could change the state, or trigger side-effects even. It's a great way to allow for a high level of customization, without all the props.

Here's how it could look:

function MyCustomDropdown(props) {
  const stateReducer = (state, action) => {
    if (action.type === Dropdown.actions.CLOSE) {
      buttonRef.current.focus();
    }
  };
  return (
    <>
      <Dropdown stateReducer={stateReducer} {...props} />
      <Button ref={buttonRef}>Open</Button>
    </>
}

You can of course create simpler ways of reacting to events, by the way. Providing an onClose prop in the previous example would probably make for a better user experience. Save the state reducer pattern for when it's required.

6. Spread the remaining props

Whenever you create a new component - make sure to spread the remaining props onto whatever element makes sense.

You don't have to keep on adding props to your component that's just going to be passed on to the underlying component or element. This will make your API more stable, removing the need for tons of minor version bumps for whenever the next developer needs a new event listener or aria-tag.

You can do it like this:

function ToolTip({ isVisible, ...rest }) {
  return isVisible ? <span role="tooltip" {...rest} /> : null;
}

Whenever your component is passing a prop in your implementation, like a class name or an onClick handler, make sure the external consumer can do the same thing. In the case of a class, you can simply append the class prop with the handly classnames npm package (or simple string concatenation):

import classNames from 'classnames';
function ToolTip(props) {
  return (
    <span 
      {...props} 
      className={classNames('tooltip', props.tooltip)} 
    />
}

In the case of click handlers and other callbacks, you can combine them into a single function with a small utility. Here's one way of doing it:

function combine(...functions) {
  return (...args) =>
    functions
      .filter(func => typeof func === 'function')
      .forEach(func => func(...args));
}

Here, we create a function that accepts your list of functions to combine. It returns a new callback that calls them all in turn with the same arguments.

You'd use it like this:

function ToolTip(props) {
  const [isVisible, setVisible] = React.useState(false);
  return (
    <span 
      {...props}
      className={classNames('tooltip', props.className)}
      onMouseIn={combine(() => setVisible(true), props.onMouseIn)}
      onMouseOut={combine(() => setVisible(false), props.onMouseOut)}
    />
  );
}

7. Give sufficient defaults

Whenever you can, make sure to provide sufficient defaults for your props. This way, you can minimize the amount of props you have to pass - and it simplifies your implementation a great deal.

Take the example of an onClick handler. If you're not requiring one in your code, provide a noop-function as a default prop. This way, you can call it in your code as if it was always provided.

Another example could be for a custom input. Assume the input string is an empty string, unless provided explicitly. This will let you make sure you're always dealing with a string object, instead of something that's undefined or null.

8. Don't rename HTML attributes

HTML as a language comes with its own props - or attributes, and it is in itself the API of the HTML elements. Why not keep using this API?

As we mentioned earlier, minimizing the API surface and making it somewhat intuitive are two great ways of improving your component APIs. So instead of creating your own screenReaderLabel prop, why not just use the aria-label API already provided to you?

So stay away from renaming any existing HTML attributes for your own "ease of use". You're not even replacing the existing API with a new one - you're adding your own on top. People could still pass aria-label alongside your screenReaderLabel prop - and what should be the final value then?

As an aside, make sure to never override HTML attributes in your components. A great example is the <button /> element's type attribute. It can be submit (the default), button or reset. However, a lot of developers tend to re-purpose this prop name to mean the visual type of button (primary, cta and so on).

By repurposing this prop, you have to add another override to set the actual type attribute, and it only leads to confusion, doubt and sore users.

Believe me - I've done this mistake time and time again - it's a real booger of a decision to live with.

9. Write prop types (or types)

No documentation is as good as documentation that lives inside your code. React comes fully kitted out with a great way to declare your component APIs with the prop-types package. Now, go use it.

You can specify any kind of requirement to the shape and form of your required and optional props, and you can even improve it further with JSDoc comments.

If you skip a required prop, or pass an invalid or unexpected value, you'll get runtime warnings in your console. It's great for development, and can be stripped away from your production build.

If you're writing your React apps in TypeScript or with Flow, you get this kind of API documentation as a language feature instead. This leads to even better tooling support, and a great user experience.

If you're not using typed JavaScript yourself, you should still consider providing type definitions for those consumers that do. This way, they'll be able to use your components much more easily.

10. Design for the developers

Finally, the most important rule to follow. Make sure your API and "component experience" is optimized for the people that will use it - your fellow developers.

One way to improve this developer experience is to provide ample error messages for invalid usage, as well as development-only warnings for when there are better ways to use your component.

When writing your errors and warnings, make sure to reference your documentation with links or provide simple code examples. The quicker the consumer can figure out what's wrong and how to fix it, the better your component will feel to work with.

Turns out, having all of these lengthy errors warnings doesn't affect your final bundle size at all. Thanks to the wonders of dead code elimination, all of this text and error code can be removed when building for production.

One library that does this incredibly well is React itself. Whenever you forget to specify a key for your list items, or misspell a lifecycle method, forget to extend the right base class or call hooks in an indeterminate way - you get big thick error messages in the console. Why should the users of your components expect anything less?

So design for your future users. Design for yourself in 5 weeks. Design for the poor suckers that have to maintain your code when you're gone! Design for the developer.

A recap

There are tons of great stuff we can learn from classic API design. By following the tips, tricks, rules and commandments in this article, you should be able to create components that are easy to use, simple to maintain, intuitive to use and extremely flexible when they need to be.

What are some of your favorite tips for creating cool components?

Discussion

pic
Editor guide
Collapse
dance2die profile image
Sung M. Kim

Thank you @selbekk for the thorough "10 component commandments".

What are some of your favorite tips for creating cool components?

I am not sure if this is a tip or a bad practice, but I've started seeing a lot of code with following structure in "return/render" methods.

{isError && <ErrorMessage error={error}}
{isLoading && <Loading />}
{!isLoading && (
  <OtherComponentRequringData data={data} />
)}

Would such a way of showing components a bad practice?
I found it more declarative then having "if/else" (or using a ternary operator).

But the downside I found is that, many people aren't familiar with how to interpret the "state && component".

What would you think?

Collapse
iansanwich profile image
Ian Esparrago

It's the short-circuit operator. I think it's neat.

background-color: ${p =>
      (p.attendanceStatus === "in" && p.theme.color.primary.main) ||
      (p.attendanceStatus === "out" && p.theme.color.out) ||
      (p.attendanceStatus === "later" && p.theme.color.warning) ||
      (p.attendanceStatus === "late" && p.theme.color.error) ||
      (p.attendanceStatus === "absent" && p.theme.color.absent) ||
      (p.attendanceStatus === "off" && p.theme.color.grey.medium)};
Collapse
ambroseus profile image
Eugene Samonenko

also found useful this pattern as more declarative, but need to use it carefully. my early pitfall was to use array.length && ... instead of array.length === 0 && ...

Collapse
andrii2019 profile image
andrii2019

What about just using
!!array.length && ...?
It looks smarter and shortly.

Thread Thread
ambroseus profile image
Eugene Samonenko

yep, it's shorter :) use !! many years while coding on perl. now I prefer Boolean(array.length)

Collapse
thewix profile image
TheWix

This is great advice. Falsey and Thruthy values bit me in the same way. I do things like isEmpty(array) or if I am really defensive isNullOrEmpty(array)

Collapse
andrii2019 profile image
andrii2019

What about just using !!array.length && ... ? It looks smarter and short.

Collapse
edwincarbajal profile image
Edwin Carbajal

I had the same question 🤔

Collapse
seanmclem profile image
Seanmclem

Does eslint still flag it?

Collapse
pedromass profile image
Pedro Mass

For Tip 2. Allow for contextual semantics

function Grid({ as: Element, ...props }) {
  return <Element className="grid" {...props} />
}
Grid.defaultProps = {
  as: 'div',
};

What would the propType be?

Something like this?

Grid.propTypes = {
  as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType])
};
Collapse
asherccohen profile image
Asher Cohen

This was the one I preferred. But in a non-typescript world that would require a lot of conditions to return the right element type.

Am I right?

Collapse
selbekk profile image
selbekk Author

Good question! It’s not a very easy type to get right, especially if you want the correct props of the as element to be applied as well. It is doable though, with a drizzle of keyof and some other ts ninja tricks. I’m not a great TS dev yet, and I’m on my phone atm.

There is lots of prior art here - look at reach UI or styled components’types for possible impoementations

Collapse
selbekk profile image
selbekk Author

Yeah that would be it!

Collapse
falldowngoboone profile image
Ryan Boone

One of the rules I try to follow is don’t make something a shared component until I’ve copied and pasted it four times. Too often I would create components too early, without any context beyond the current one. This would lead to components with awkward and bloated APIs from other developers trying to adapt it to their needs.

Also, if your component encapsulates complicated code structure, offer a way for developers to get to the individual components. For example, say you’ve created an Input component that renders a label and control, as well as an error message on invalid input. Give developers a way to access the individual components and allow them to completely recompose (or add to) the component’s structure if they have to.

Some library APIs give you render props for each component, but that quickly gets messy when you have to add a labelRender, a controlRender, and an errorRender. I personally like compound components exported from the original component (e.g. <Input.Label>), along with React Context providing the necessary linking state (e.g. the input’s ID for the label’s htmlFor prop).

Collapse
allpro profile image
Kevin Dalman

I... don’t make something a shared component until I’ve copied and pasted it four times... This would lead to components with awkward and bloated APIs from other developers trying to adapt it to their needs.

The problems with this are:

  1. Your duplicated code likely has minor updates in each iteration, instead of refininements to a 'common component'. Now it is more difficult to unify them.
  2. If you needed the same functionality 4 times, it's likely other team devs did as well. Since there was no common component, each had to reinvent the wheel, each time with different API's. Now it's very difficult to unify all these components to use the same common one, along with all their tests. This is so much work that it usually is never done, and the duplicated code remains, complicating future enhancements and maintenance.

I find it best to create common components as early as possible, then encourage reuse rather that duplication, (DRY). Common components should evolve as new needs arise; this is agile coding! Even heavily used libraries like MaterialUI are updated regularly, so your own 'library' of components should be as well.

Good code reviews can prevent overcomplicating common components. Guidelines like the ones in this article, along with app standards, also help keep API's simple and consistent across an app. It's easier to refactor a common component occasionally than to replace totally different implementations of the same functionality.

Collapse
falldowngoboone profile image
Ryan Boone

I agree that good code reviews can prevent over complicating common components, but I’m not as concerned with reinventing the wheel, at least not at first. I believe components need to earn their right to exist, and by the time they have earned this right, you typically have a good starting point for an API, and a good reviewer can pick this out.

Also, in my experience, if I am having difficulty unifying components into a common component, there’s a good chance they aren’t as common as I might have originally thought. I may be better served in extracting out common related logic into a helper function or hook.

With the projects I work on at work, I tend to fall on the side of making code as disposable as possible, so take what I’m with a grain of salt. I am very hard on what makes its way into our shared common components.

Honestly, whatever works for you is great, especially since this is one of those areas where it depends a lot on the context of the work.

Collapse
ambroseus profile image
Eugene Samonenko

my favorite tip with redux connect & compose to reduce boilerplate (with HOCs):

const mapStateToProps = state => ({ ... });

export default compose(
  withRouter,
  withStyles,
  ...
  connect(mapStateToProps)
)(MyComponent);
Collapse
ganevru profile image
Ivan Ganev

Great article, thanks.

As I understand it, this is a small typo (tooltipp)?

className = {classNames ('tooltipp', props.className)}

I especially liked the eighth recommendation - about HTML attributes. Does anyone know if there is an eslint rule for this (for react)? And is it even possible to make such eslint rule? At first glance, this is possible.

Collapse
selbekk profile image
selbekk Author

Hi Ivan! Yeah, thanks, I’ll correct it.

I’m not sure about an eslint rule, bur it should be doable I guess :-)

Collapse
kontsedal profile image
Bohdan

Nice article, thanks. I'm not sure that the usage of magic strings is a good practice:

<Button variant="primary" size="large">
  I am primarily a large button
</Button>

Wouldn't it be better? :

<Button variant={Button.VARIANTS.PRIMARY} size={Button.SIZES.LARGE}>
  I am primarily a large button
</Button>
Collapse
selbekk profile image
selbekk Author

That’s a nice way to do it, too. I tend to use prop types or type annotations to make that assertion for me, which reads better to me.

Collapse
maciekgrzybek profile image
Maciek Grzybek

Great article mate 👌

Collapse
pajasevi profile image
Pavel Ševčík

Terrific post! Thank you!

Collapse
palnes profile image
Pål Nes

Just a heads-up that Storybook will get an MDX documentation add-on in the next version. We're running the technical preview at our end. :)

Collapse
trondeh80 profile image
Trond Erling Hundal

Awesome post! Inspiring work dude!

Collapse
michaelaubry profile image
Michael Aubry

This is great!