5 ways to create a self-documenting React component

Featured on Hashnode

Subscribe to my newsletter and never miss my upcoming articles

A React component is well documented when its purpose and intended usage are clearly communicated. To be self-documenting, a component must simply convey as much relevant information as possible through its own code.

Without further ado, here are five ways to create self-documenting React components.

1. Types

Using prop types is always a good idea. It helps us detect unexpected props and catch potential bugs in our application.

But prop types are also a documentation tool. They give us essential information about how we expect a component to be used. Let's look at a simple button:

const Button = ({ onClick, label, icon }) => {
    return (
        <button onClick={onClick}>
            {label}
            {icon}
        </button>
    )
}

Button.propTypes = {
    onClick: PropTypes.func.isRequired,
    label: PropTypes.string.isRequired,
    icon: PropTypes.node
}

We get a lot from the prop types here. We know that we should't render the button without a label (which should be a string) or an onClick handler (a function) because they are required. We can skip the icon, though, if we want.

However, if we are not careful, we can still miss the console warnings and use our Button in ways that are now 'allowed'. How can we prevent that?

This is where static typing comes into the picture. Using Typescript, as opposed to just prop types, means we know whether there is a problem with our types at compile time.

This behaviour makes it (almost) impossible for our type definition to become outdated. We can reliably use it as a guide to the intended usage of our component. Not to mention the added bonus of autocompleting props in our IDE.

I like to think of Typescript as a way to enforce documentation.

Let's get a taste of what it looks like:

type ButtonProps = {
    onClick: () => void
    label: string
    icon?: React.ReactNode
}

const Button = ({ onClick, label, icon }: ButtonProps) => {
    return (
        <button onClick={onClick}>
            {label}
            {icon}
        </button>
    )
}

2. Tests

Unit tests are one of the best ways to convey the intended functionality of a component. To be good a documentation tool, tests should focus on behavior, not implementation details.

Let's use the HiddenMessage test from the React Testing Library docs as an example:

import React from 'react'
import {render, fireEvent, screen} from '@testing-library/react'
import HiddenMessage from '../hidden-message'

test('shows the children when the checkbox is checked', () => {
  const testMessage = 'Test Message'
  render(<HiddenMessage>{testMessage}</HiddenMessage>)

  expect(screen.queryByText(testMessage)).toBeNull()

  fireEvent.click(screen.getByLabelText(/show/i))

  expect(screen.getByText(testMessage)).toBeInTheDocument()
})

Even without the context of the React component itself, its functionality is clear. We have an input with label "show" which, if clicked, will show the hidden message. The test contains all the information we need to understand the behavior. It serves as, de facto, documentation.

In more complex situations, where there is a lot of interaction and interdependence between components, unit testing might not be enough. In that case, integration tests (even e2e tests) can be an awesome tool to document entire features. Cypress would be my library of choice in this scenario.

3. Storybook

Storybook is awesome! It's a tool that allows us to develop UI components in isolation. Creating a story can be a convenient way to document every possible state of a component. And all user interactions, as well.

What does a story look like? Here's an example:

import { select, text } from '@storybook/addon-knobs'

import Notification from 'components/Notification'

export const Primary = () => (
        <Notification
            color={select('Color', ['error', 'primary'], 'error')}
            title={text('Title', 'Information')}
            description={text(
                'Some description,
            )}
        />
)

Primary.storyName='Primary notification';

We can immediately understand the purpose of this component when looking at the code. Clearly, we can add a title and a description to the notification and also choose one of the two available colors. Using Storybook, we can also play with it in the browser. Since this is a real implementation of our code, we are sure to keep it up to date as we change our component.

As a bonus, Storybook will also automatically generate a Docs page for each component. Learn more about Storybook docs here .

4. Names

Component names are important. We want our code to be self-explanatory. But this is such a trivial point. Why are we even mentioning it? We would never name our components in a non-obvious way.

The key issue here is that beauty is in the eyes of the beholder. Whether components are well-named depends largely on the perception of the person who is reading the code, not the one who is writing it.

Here's what I mean. Let's say we want to build a Table component. We might create an API like this:

const TableExample = () => {
    return (
        <Table>
            <Header>
                <Cell>....</Cell>
                <Cell>....</Cell>
            </Header>
            <Row>
                <Cell>....</Cell>
                <Cell>....</Cell>
            </Row>
        </Table>
    )
}

While writing this code, we have the full context of what we are doing. Therefore, for us, it is self-evident that when I refer to a Row or a Header, these are part of our Table.

But imagine we are working on a project with many complex UI elements. Row or Header can refer to different components in a more general context. Other developers on our team can become justifiably confused. How can we fix it?

How about we do this instead:

const TableExample = () => {
    return (
        <Table>
            <TableHeader>
                <TableCell>....</TableCell>
                <TableCell>....</TableCell>
            </TableHeader>
            <TableRow>
                <TableCell>....</TableCell>
                <TableCell>....</TableCell>
            </TableRow>
        </Table>
    )
}

With this small tweak we eliminate all ambiguity and document the intended usage of the components directly in their names. We can now be certain we have properly communicated their purpose.

5. File structure

There is one more way we can convey the why and how of a component within a codebase - by placing it in a specific location within the file structure.

Let's imagine we have a simple React app with two separate pages. The two pages share certain UI elements but are otherwise unique.

A simplified version of the app file structure might look something like this:

├── components
├── pages
│   ├── About
│   │   ├── components
│   │   ├── About.js
│   ├── Home
│   │   ├── components
│   │   ├── Home.js
├── App.js

We have three component folders. If we create a new component, where do we put it? Our choice communicates whether the new component belongs to the Home page, the About page or both. We have, in effect, used our file structure as a documentation tool.

Conclusion

This list is far from exhaustive. It is only meant as an illustration of how we can think of our components as communication tools. The choices we make in our code tell a story. And what is a story, if not a document.

Although I focused heavily on React components in the examples above, most of these methods are certainly transferable. Their application goes well beyond React and arguably even beyond web development.

What are your favourite ways to document React components?

Let me know in the comments below or on Twitter.

Space Aardvark's photo

Great guide! Tips everyone can use.

Iva's photo

Thanks so much! I am happy you found it useful 🙂

Edidiong Asikpo's photo

Very insightful article Iva. I enjoyed reading it.

Iva's photo

Thanks! I am really glad you enjoyed it! 😊