React Component Patterns

Mar 12, 2022reactpatterns

If you’ve been writing React for a while, you have probably already discovered all following component patterns which I find immensely useful when writing React applications.

While this is not an exhaustive list, it applies to most problems you will probably encounter when building components.

Hooks

Hooks let you write functional component that can handle its state like classes. As well it introduces the ability to create side effects like data fetching, subscriptions, or manually changing the DOM.

import React, {useState} from 'react'

function Example() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

Hooks let you split one component into smaller functions based on what pieces are related (such as setting up a subscription or fetching data), rather than forcing a split based on lifecycle methods.

Hooks are JavaScript functions, but you need to follow two rules when using them:

  • Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions.
  • Only call Hooks from React function components or your custom Hooks. Don’t call Hooks from regular JavaScript functions or classes. Hooks do not work inside classes.

Context

Context provides a way to share values between components without having to explicitly pass a prop through every level of the tree.

const ThemeContext = React.createContext('light')

class App extends React.Component {
  render() {
    // Use a Provider to pass the current theme to the tree below.
    // Any component can read it, no matter how deep it is.
    // In this example, we're passing "dark" as the current value.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    )
  }
}

// A component to consume context
function Content() {
  return (
    <ThemeContext.Consumer>
      {(theme) => <ProfilePage theme={theme} />}
    </ThemeContext.Consumer>
  )
}

Context is designed to share data that can be considered global for a tree of React components, such as the current authenticated user, theme, or preferred language.

Because context uses reference identity to determine when to re-render, there are some gotchas that could trigger unintentional renders in consumers when a provider’s parent re-renders.

Render Props

The basic idea of the pattern is that rather than have the toggle component be responsible for doing anything special in the render method, we delegate that responsibility over to the user and we give them the state and functions necessary to allow the user of the component to render what they need for their use case.

Render Props is a simple technique for sharing code between components using a prop whose value is a function. The below component uses render prop which returns a React element.

<DataProvider render={(data) => <h1>{`Hello ${data.target}`}</h1>} />

One interesting thing to note about render props is that you can implement most higher-order components (HOC) using a regular component with a render prop.

function withMouse(Component) {
  return class extends React.Component {
    render() {
      return (
        <Mouse
          render={(mouse) => <Component {...this.props} mouse={mouse} />}
        />
      )
    }
  }
}

It’s important to remember that just because the pattern is called Render Props you don’t have to use a prop named render to use this pattern. In fact, any prop that is a function that a component uses to know what to render is technically a Render Prop”.

// use the children prop
<Mouse
  children={(mouse) => (
    <p>
      The mouse position is {mouse.x}, {mouse.y}
    </p>
  )}
/>

// or put it directly inside the element
<Mouse>
  {mouse => (
    <p>The mouse position is {mouse.x}, {mouse.y}</p>
  )}
</Mouse>

Using a render prop can negate the advantage that comes from using React.PureComponent if you create the function inside a render method. This is because the shallow prop comparison will always return false for new props, and each render in this case will generate a new value for the render prop.

Higher Order Components (HOC)

This pattern is pretty widely popular, heavily used in react-redux. HOC doesn’t modify the input component, nor does it use inheritance to copy its behavior. Rather, a HOC composes the original component by wrapping it in a container component. A HOC is a pure function with zero side-effects.

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Current props: ', this.props)
      console.log('Previous props: ', prevProps)
    }
    render() {
      return <WrappedComponent {...this.props} />
    }
  }
}

const EnhancedComponent = logProps(InputComponent)

Higher-order components come with a few caveats that aren’t immediately obvious if you’re new to React.

  • Don’t use HOCs inside the render method. Instead, apply HOCs outside the component definition so that the resulting component is created only once. Then, its identity will be consistent across renders.
  • Static methods must be copied over. When you apply a HOC to a component, the new component does not have any of the static methods of the original component. You could copy all static methods onto the container before returning it by using hoist-non-react-statics.
  • Refs aren’t passed through. That’s because ref is not really a prop, it’s handled specially by React. You must use the React.forwardRef to manually forward refs.

Compound Components

Instead of jamming all props in one giant parent component and drilling those down to child UI components, here each prop is attached to the SubComponent that makes the most sense.

import {Card, ListGroup} from 'react-bootstrap'

const Foo = (props) => (
  <Card style={{width: '18rem'}}>
    <Card.Header>Featured</Card.Header>
    <ListGroup variant="flush">
      <ListGroup.Item>one</ListGroup.Item>
      <ListGroup.Item>two</ListGroup.Item>
      <ListGroup.Item>three</ListGroup.Item>
    </ListGroup>
  </Card>
)

Your component has great UI flexibility, allowing the creations of various cases from a single component. Having flexibility comes along with the possibility to provoke unexpected behavior.