React: Senior devs write small components! 🚀
Tags: react, javascript
Published: 2022-02-15 06:19:20

Rather than have a few components that do many things, we should prefer to create many smaller components that we can put together (compose) to achieve the desired effect.

> TL;DR: Whenever your component starts accepting too many props, it's usually a sign that the component could be broken up into separate parts, where each part is only concerned with one thing. **By separating out the components they become easier to read, re-usable in other areas, and easily extended/overridden when required.**

## A large component

**PageHeader.tsx**

```tsx
import React from 'react'
import styled from 'styled-components'
import {useMediaQuery} from '@material-ui/core'
import {breakpoints} from 'lib/ui/theme'
import Button from 'lib/ui/Button'
import {Title, Description} from 'lib/ui/typography'

export type PageHeaderProps = {
  disabled?: boolean
  title: string
  smTitle?: string
  buttonText: string
  smButtonText?: string
  description?: string
  'aria-label'?: string
  onClick?: () => void
}

export default function PageHeader(props: PageHeaderProps) {
  const type = props.onClick ? 'button' : 'submit'
  const matches = useMediaQuery(`(max-width: ${breakpoints.sm})`)
  const title = matches && props.smTitle ? props.smTitle : props.title
  const buttonText =
    matches && props.smButtonText ? props.smButtonText : props.buttonText

  const DescriptionBox = () => {
    if (props.description) {
      return (
        <StyledBox>
          <Description>{props.description}</Description>
        </StyledBox>
      )
    }

    return null
  }

  return (
    <Container>
      <MDFlexBox>
        <Title>{title}</Title>
        <Button
          type={type}
          variant="contained"
          color="success"
          aria-label={props['aria-label'] ? props['aria-label'] : 'submit'}
          disabled={props.disabled}
          onClick={props.onClick}
        >
          {buttonText}
        </Button>
      </MDFlexBox>
      <SMFlexBox>
        <Title>{props.smTitle ? props.smTitle : props.title}</Title>
        <Button
          type={type}
          variant="contained"
          color="success"
          aria-label={props['aria-label'] ? props['aria-label'] : 'submit'}
          disabled={props.disabled}
          onClick={props.onClick}
        >
          {props.smButtonText ? props.smButtonText : props.buttonText}
        </Button>
      </SMFlexBox>
      <DescriptionBox />
    </Container>
  )
}
```

Contains lots of behavior:

- Header layout info
- Title values for various widths
- Button info
- Conditionally rendering via a nested component
- This approach has also had to duplicate the components to handle the different layouts. Duplication is generally bad, let's avoid it where we can.

We could say this component is very specific. It only renders a single layout, and pre-defined children. Any variations would either require:

- Copy-pasting behavior
- Adding new props, then using more ifs, or other logical operators to determine what to render/style.

**Using PageHeader.tsx**

```tsx
<PageHeader
  onClick={save}
  disabled={processing}
  title="Add form"
  buttonText="Save Changes"
  smButtonText="Save"
  aria-label="save form"
/>
```

- Not sure what is being clicked / disabled / label: button? title?
- Results in many props with long names - buttonText, smButtonText
- Need to go into <PageHeader>, and scan a lot of code to find out when smButtonText is rendered.

## Using smaller components

Let's start with how we'd like to use it.

```tsx
<PageHeader>
  <Title text="Add form"/>
  <Button aria-label="save form" 
          onClick={save} 
          disabled={processing}
          text="Save Changes"
          textCollapsed="Save"
  />
</PageHeader>
```

- `<PageHeader>` is only concerned with the layout
- Clearer where each prop is applied.
- If we're only interested in <Title> behavior, we'll only need to look at that component.
- Smaller, clearer prop names.
- We know the title will eventually need a textCollapsed too, so we'll just use a text prop to keep it consistent with the button


**PageHeader/index.tsx**

```tsx
export default function PageHeader(props: {children: JSX.Element[]}) {
  return <Container>{props.children}</Container>
}

const Container = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: ${(props) => props.theme.spacing[21]} !important;

  @media (min-width: ${(props) => props.theme.breakpoints.sm}) {
    margin-bottom: ${(props) => props.theme.spacing[19]} !important;
  }
`
```

- Only concerned with layout

**PageHeader/Title.tsx**

```tsx
export default function Title(props: {text: string; textCollapsed?: string}) {
  const {text, textCollapsed} = props

  return (
    <>
      <DesktopTitle>{text}</DesktopTitle>
      <Include if={Boolean(textCollapsed)}>
        <MobileTitle>{textCollapsed}</MobileTitle>
      </Include>
    </>
  )
}

const DesktopTitle = DesktopOnly(TitleText)
const MobileTitle = MobileOnly(TitleText)
```

- Only concerned with title related behavior
- Not mixing style with rendering. Not using media query / breakpoints inside component body.

**DesktopOnly/MobileOnly**

Style utility component that wraps whatever component you pass in, so that it only shows at the given width.

```tsx
export const DesktopOnly = (component: React.FC<any>) => styled(component)`
  display: none;

  @media screen and (min-width: ${(props) => props.theme.breakpoints.sm}) {
    display: block;
  }
`
```
- Only concerned with showing/hiding at various breakpoints

**PageHeader/Button.tsx**

Similar to title, but we'll also extend the base `<Button>`, and set some default props.

```tsx
export default function Button(
  props: Partial<ButtonProps> & {
    text: string
    textCollapsed?: string
  },
) {
  const {text, textCollapsed, ...buttonOverrides} = props

  const buttonProps: Partial<ButtonProps> = {
    variant: 'contained',
    color: 'success',
    ...buttonOverrides,
  }

  return (
    <>
      <DesktopButton {...buttonProps}>{text}</DesktopButton>
      <Include if={Boolean(textCollapsed)}>
        <MobileButton {...buttonProps}>{textCollapsed}</MobileButton>
      </Include>
    </>
  )
}
```

- Props can still be overridden.
- Clear what is rendered where, and when.