DEV Community

Ben McMahen
Ben McMahen

Posted on • Originally published at benmcmahen.com on

Three Ways to use Hooks to Build Better React Components

This article provides three ways in which you can replace various old React composition patterns with hooks to build simpler, more reusable components. Before starting, it helps to have some experience with various patterns related to component composition, including making use of children, using render props, higher order components, and cloning elements. And it helps if you have an introductory understanding of hooks.

Tip 1: Replace cloneElement with hooks

You can often replace instances of cloneElement, higher order components, or render props with hooks.

The motivation of using one of the above techniques is almost always to provide either some contextual information to child components or to expose some additional logic to components. React's cloneElement function is probably the oldest means to achieve this end, but it's always had some downsides: 1) It's usage is dependent on the child being of the correct type. 2) It can overrwrite props, necessitating that you wrap potential props to ensure that each one is applied correctly. 3) It's difficult to properly type child components when using something like Typescript.

Let's explore a better solution using hooks. Let's say we want to make a child component aware of which parent it's in when developing a table so that we can use the correct tagName, either an td or th. Consider the cloneElement way to achieve this:

const TableHead = ({ children }) => {
  return (
    <thead>
      {React.cloneElement(children, {
        parent: 'TableHead',
      })}
    </thead>
  )
}

const TableBody = ({ children }) => {
  return (
    <tbody>
      {React.cloneElement(children, {
        parent: 'TableBody',
      })}
    </tbody>
  )
}

const TableCell = ({ parent, children }) => {
  const Component = parent === 'TableHead' ? 'th' : 'td'
  return <Component>{children}</Component>
}

This works decently enough. We can create a table and the correct tagNames are used in each case.

const Table = () => (
  <table>
    <TableHead>
      <TableCell>Name</TableCell>
      <TableCell>Age</TableCell>
    </TableHead>
    <TableBody>
      <TableCell>Ben McMahen</TableCell>
      <TableCell>Thirty-something</TableCell>
    </TableBody>
  </table>
)

We can provide a more flexible solution using hooks and context. Let's rewrite our components to demonstrate:

const SectionContext = React.createContext({ parent: 'TableHead' })

const TableHead = ({ children }) => {
  return (
    <thead>
      <SectionContext.Provider value={{ parent: 'TableHead' }}>
        {children}
      </SectionContext.Provider>
    </thead>
  )
}

const TableBody = ({ children }) => {
  return (
    <tbody>
      <SectionContext.Provider value={{ parent: 'TableBody' }}>
        {children}
      </SectionContext.Provider>
    </tbody>
  )
}

const TableCell = ({ children }) => {
  const { parent } = React.useContext(SectionContext)
  const Component = parent === 'TableHead' ? 'th' : 'td'
  return <Component>{children}</Component>
}

This is a more flexible solution because it doesn't depend on TableCell being a direct descendent of either TableHead or TableBody. It's also great if you're using typescript because it doesn't polute your TableCell props with props that are provided by the parent component.

Tip 2: Bind elements to refs

Return a bind function from your hooks to make reference to dom elements.

I first came across this pattern in react-spring and I've used it a ton since. Consider cases where you want to create reusable functionality which makes reference a particular dom element, such as measuring dom elements or focusing them. In my case, I recently needed to create a reusable focus manager that binds to a particular element and either focuses an element if it's showing or returns focus if it's not. Focus trap is a great tool for helping us here. Let's start with a basic hook skeleton.

export function useFocusElement(showing, options = {}) {
  const elementRef = React.useRef(null)

  return {
    bind: { ref: elementRef },
  }
}

So yeah, this doesn't do much. It returns a bind object which includes a reference to our elementRef. This will allow us to create a reference to any dom element that we want to focus. The showing argument will be used to determine if we should assign focus to the elementRef or return it to the element originally focused. We can use the hook as follows:

const Popover = () => {
  const [showing, setShowing] = React.useState(false)
  const bind = useFocusElement(showing)
  return <div {...bind}>Popover!</div>
}

Let's implement the rest of the hook to make use of focus trap:

export function useFocusElement(showing, options = {}) {
  const elementRef = React.useRef(null)
  const trapRef = React.useRef(null)

  function focusElement() {
    const trap = createFocusTrap(elementRef.current, {
      escapeDeactivates: false,
      clickOutsideDeactivates: false,
      fallbackFocus: '[tabindex="-1"]',
      ...options,
    })

    trapRef.current = trap
    trap.activate()
  }

  function focusTrigger() {
    trapRef.current.deactivate()
  }

  React.useEffect(
    () => {
      if (showing) focusElement()
      else focusTrigger()
    },
    [showing]
  )

  return {
    bind: { ref: elementRef },
  }
}

So here's what's happening: We create two refs: our elementRef is binding to our popup menu, while our trapRef is referencing our focus trap instantiation. When the showing prop changes, we either focus our elementRef or we return focus to the trigger element.

This hook doesn't create any additional dom elements and it's incredibly easy to reuse within different components when you need to manage focus for accessibility reasons. I've used it in a popover, modal, and dropdown menu. I recommend being consistent in using a bind object which includes things like ref but which can also include additional functions such as onKeyDown, onMouseOver, etc.

You can see the full implementation of the useFocus hook in sancho-ui, and see how it's used in the Popover implementation.

Tip 3: useState takes a callback

When using useState with a callback, the callback is run only on the initial mount. This is useful for running expensive computations. Compare these two examples:

let counter = 0

function Component() {
  const [number] = useState(counter++)
  return <div>{number}</div>
}

With this example, any time our Component updates our counter will be incremented. Note that this will not update our number state, since default state values can only ever be set once on the initial mount of the component.

Let's convert useState to use a callback.

let counter = 0

function Component() {
  const [number] = useState(() => counter++)
  return <div>{number}</div>
}

This will only increment our uid once during the entire lifespan of that component, even if it rerenders. Like the above example, our number will remain constant.

The sandbox below demonstrates these differences:

(This is an article posted to my blog at benmcmahen.com. You can read it online by clicking here.)

Top comments (0)