React
Hooks

Components

Components allow us to break more complex problems into simpler reusable pieces that we can compose together.

 

React class components  have lifecycle methods that we can tap into to perform certain actions

class Foo extends Component {

  componentDidMount () {
    this.#isMounted = true
    this.setState({ isLoading: true })
    fetchSomething(this.props.resourceId)
      .then((data) => 
        this.#isMounted &&
        this.setState({ data, isLoading: false }))
      .catch((error) => 
        this.#isMounted &&
        this.setState({ error, isLoading: false}))
  }

  componentDidUpdate (prevProps) {
    if (prevProps.resourceId !== this.props.resourceId) {
      fetchSomething(this.props.resourceId)
    }
  }

  componentWillUnmount () {
    this.#isMounted = false
  }

  render () {
    return (
      <pre>
        <code>
          {data || error}
        </code>
      </pre>
    )
  }

}

Components

Lifecycle methods force us to spread logic, such as fetching and displaying data from an endpoint, across the lifecycle methods of the  component. This works, but it prevents us from encapsulating that work and reusing it else where.

import React, { Component } from 'react'

class Foo extends Component {
  componentDidMount () {
    this.#isMounted = true
    this.setState({ isLoading: true })
    fetchSomething(this.props.resourceId)
      .then((data) => 
        this.#isMounted &&
        this.setState({ data, isLoading: false }))
      .catch((error) => 
        this.#isMounted &&
        this.setState({ error, isLoading: false}))
  }

  componentDidUpdate (prevProps) {
    if (prevProps.resourceId !== this.props.resourceId) {
      fetchSomething(this.props.resourceId)
    }
  }

  componentWillUnmount () {
    this.#isMounted = false
  }

  render () {
    return (
      <pre>
        <code>
          {this.state.data || this.state.error}
        </code>
      </pre>
    )
  }

}

HOC

Higher order components were one of the first patterns to emerge that allowed us to encapsulate related logic and make it shareable.

import React from 'react'

export const Foo = ({ data, error }) => (
  <pre>
    <code>
      {data || error}
    <code>
  </pre>

export default withResource(resourceId)(Foo)

We could encapsulate sharable logic in an outer component, and render the inner component passing in the related data.

import React, { Component } from 'react'

export default (ComponentIn) => class Foo extends Component {
  componentDidMount () {
    this.#isMounted = true
    this.setState({ isLoading: true })
    fetchSomething(this.props.resourceId)
      .then((data) => 
        this.#isMounted &&
        this.setState({ data, isLoading: false }))
      .catch((error) => 
        this.#isMounted &&
        this.setState({ error, isLoading: false}))
  }

  componentDidUpdate (prevProps) {
    if (prevProps.resourceId !== this.props.resourceId) {
      fetchSomething(this.props.resourceId)
    }
  }

  componentWillUnmount () {
    this.#isMounted = false
  }

  render () {
    return (
      <ComponentIn
        {...this.props}
        data={this.state.data}
        error={this.state.error}
      />)
  }

}

FaC/Render Props

The function as child, followed quickly by render props became another way to encapsulate logic and make it sharable.

import React from 'react'
import Resource from './resource'

export const Foo = ({ resourceId }) => (
  <Resource resourceId={resourceId}>
    {({ data, error }) => (
      <pre>
        <code>
          {data || error}
        <code>
      </pre>
    )
  </Resource>

export default withResource(resourceId)(Foo)
import React, { Component } from 'react'

export default class Resource extends Component {
  componentDidMount () {
    this.#isMounted = true
    this.setState({ isLoading: true })
    fetchSomething(this.props.resourceId)
      .then((data) => 
        this.#isMounted &&
        this.setState({ data, isLoading: false }))
      .catch((error) => 
        this.#isMounted &&
        this.setState({ error, isLoading: false}))
  }

  componentDidUpdate (prevProps) {
    if (prevProps.resourceId !== this.props.resourceId) {
      fetchSomething(this.props.resourceId)
    }
  }

  componentWillUnmount () {
    this.#isMounted = false
  }

  render () {
    return this.props.children({
      data: this.state.data,
      error: this.state.error
    })
  }

}

In this pattern, a `render` prop or the `children` prop is expected to be a function that is called with the relevant data. The consumer then returns the desired UI.

Bespoke Solutions

Although promoted by the React team, each of this patterns were developed by the React community to fulfill a need that the React library did not provide natively.

 

It is a testament to React's flexibility and versatility that these patterns were able to emerge and gain footholds quickly.

Higher Order Components

Function as Child

Render Props

Hooks

After several years of React in the wild, and with a number of patterns emerging to solve the same kinds of problems -- each with their own benefits and drawbacks -- Hooks is the React team's official answer to several of React's long time inefficiencies, including the problem of encapsulated logic. 

useState

useEffect

useContext

...

Hooks

Like the other patterns, hooks enables us to encapsulate and reuse logic.

import React, { useState, useEffect } from 'react'

const Foo ({ resourceId }) => {
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)

  useEffect(() => {
    fetchSomething(resourceId)
      .then(setData)
      .catch(setError)
  }, [resourceId])

  return (
    <pre>
      <code>
        {data || error}
      </code>
    </pre>
  )
}

Unlike the other patterns, hooks don't use lifecycle methods, at least not directly. In fact, they can't be used in class components at all.

import React, { Component } from 'react'

class Foo extends Component {
  componentDidMount () {
    this.#isMounted = true
    this.setState({ isLoading: true })
    fetchSomething(this.props.resourceId)
      .then((data) => 
        this.#isMounted &&
        this.setState({ data, isLoading: false }))
      .catch((error) => 
        this.#isMounted &&
        this.setState({ error, isLoading: false}))
  }

  componentDidUpdate (prevProps) {
    if (prevProps.resourceId !== this.props.resourceId) {
      fetchSomething(this.props.resourceId)
    }
  }

  componentWillUnmount () {
    this.#isMounted = false
  }

  render () {
    return (
      <pre>
        <code>
          {this.state.data || this.state.error}
        </code>
      </pre>
    )
  }

}

Hooks

React exposes a number of hooks for use out of the box. These hooks are extremely useful on their own, but become even more powerful when combined to create custom hooks.

import React, { useState, useEffect } from 'react'

const Foo ({ resourceId }) => {
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)

  useEffect(() => {
    fetchSomething(resourceId)
      .then(setData)
      .catch(setError)
  }, [resourceId])

  return (
    <pre>
      <code>
        {data || error}
      </code>
    </pre>
  )
}
// use-resource.js
import { useState, useEffect } from 'react'

export default (resourceId) => {
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)

  useEffect(() => {
    fetchSomething(resourceId)
      .then(setData)
      .catch(setError)
  }, [resourceId])

  return { data, error }
}
import React from 'react'
import useResource from './use-resource'

const Foo ({ resourceId }) => {
  const { data, error } = useResource(resourceId)

  return (
    <pre>
      <code>
        {data || error}
      </code>
    </pre>
  )
}

Check out the official docs for all the available 'primitive' hooks and how they work.

Rules

For the most part, you can think of hooks as plain javascript functions that may close over some values. There are a couple additional constraints to be aware of though.

  1. Must only be used inside function components or custom hooks -- no class components or JavaScript functions that aren't React components.
     
  2. Must be declared at the top of the function (not inside any loops, conditionals or inner functions)

 

Testing

With the release of Hooks, React has taken a stance on how components should be tested.

 

"We recommend using react-testing-library which is designed to encourage writing tests that use your components as the end users do."

import React from 'react'
import { render, cleanup } from 'react-testing-library'
import * as useResourceModule from './use-resource.js'

describe('<Foo />', () => { 
  afterEach(cleanup)

  it('renders data if data is present', () => { 
    const useResourceMock = jest.spyOn(useResourceModule, 'default')
      .mockImplementation(() => ({ data: 'data' }))
    const { container, getByText} = render(<Foo />)
    expect(getByText(container, 'data')).not.toThrow()
    useResourceMock.mockRestore()
  })

  it('renders error if error is present', () => { 
    const useResourceMock = jest.spyOn(useResourceModule, 'default')
      .mockImplementation(() => ({ error: 'error' }))
    const { container, getByText} = render(<Foo />)
    expect(getByText(container, 'error')).not.toThrow()
    useResourceMock.mockRestore()
  })
})

Testing

react-testing-library provides utilities for testing custom hooks.

 

Tests for our useResource custom hook might look something like this.

import {testHook, wait, cleanup} from 'react-testing-library'
import * as fetchResourceModule from './fetch-resource'
import useResource from './use-resource'


describe('useResource', () => {
  let fetchResourceSpy
  beforeEach(() => {
    fetchResourceSpy = jest.spyOn(fetchResourceModule, 'default')
      .mockImplementation(() => Promise.resolve(42))
  })
  afterEach(() => {
    fetchResourceSpy.mockRestore()
    cleanup()
  })

  it('accepts a resource id and calls `fetchResource with it`', () => {
    testHook(() => useResource(1))
    expect(fetchResourceSpy).toBeCalledWith(1)
  })

  it('returns an object with data and error properties', () => { 
    let resource
    testHook(() => (resource = useResource(resourceId)))
    expect(resource).toEqual(expect.objectContaining({ data: null, error: null }))
  })

  it('populates data when resolved', () => {
    let resource
    testHook(() => { resource = await wait(useResource(resourceId)) })
    expect(resource).toEqual(expect.objectContaining({ data: 42, error: null }))
  })

  it('populates error when rejected', () => {
    const fetchResourceSpy = jest.spyOn(fetchResourceModule, 'default')
      .mockImplementation(() => Promise.reject('fail'))
    let resource
    testHook(() => {
      resource = await wait(() => resource = useResource(resourceId))
    })
    expect(resource).to(expect.objectContaining({ data: null, error: 'fail' }))
    fetchResourceSpy.mockRestore()
  })
})

Intent

React uses a declarative pattern for creating UI. A declarative model expresses intent -- or the "what" -- and abstracts the implementation -- or the "how". It is one of React's strongest features.

 

It has been an unfortunate truth that within the declarative composition of components, the component definitions themselves have been a jumble of imperative and declarative code.  In the `render` component method expressing the UI's intent, and in the others the implementation of that component's logic spread across several lifecycle methods.

 

The intent of hooks, is to provide a way for developers to express intent within a component, and implementation within the hooks.

?s

deck

By Cory Brown

Loading comments...

More from Cory Brown