DEV Community

Mike Wu
Mike Wu

Posted on

React: Senior devs write small components! 🚀

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

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

<PageHeader
  onClick={save}
  disabled={processing}
  title="Add form"
  buttonText="Save Changes"
  smButtonText="Save"
  aria-label="save form"
/>
Enter fullscreen mode Exit fullscreen mode
  • Not sure what is being clicked / disabled / label: button? title?
  • Results in many props with long names - buttonText, smButtonText
  • Need to go into , 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.

<PageHeader>
  <Title text="Add form"/>
  <Button aria-label="save form" 
          onClick={save} 
          disabled={processing}
          text="Save Changes"
          textCollapsed="Save"
  />
</PageHeader>
Enter fullscreen mode Exit fullscreen mode
  • <PageHeader> is only concerned with the layout
  • Clearer where each prop is applied.
  • If we're only interested in 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

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;
  }
`
Enter fullscreen mode Exit fullscreen mode
  • Only concerned with layout

PageHeader/Title.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)
Enter fullscreen mode Exit fullscreen mode
  • 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.

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

  @media screen and (min-width: ${(props) => props.theme.breakpoints.sm}) {
    display: block;
  }
`
Enter fullscreen mode Exit fullscreen mode
  • 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.

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • Props can still be overridden.
  • Clear what is rendered where, and when.

Latest comments (0)