Class Components

What?

  • class components used to be the first class citizens
  • has state, props, and lifecycle methods
  • error prone, unless dead simple

Why?

  • legacy, mainly
  • please use FC whenever possible
  • migrate class components to FC if possible

How?

render

  • examine state and props -> return JSX
  • MUST BE PURE
  • can’t use hooks 😞
import React, { Component, ReactNode } from 'react'

export interface CounterProps {
  defaultValue: number
}

interface CounterState {
  value: number
}

export class Counter extends Component<CounterProps, CounterState> {
  static defaultProps = { defaultValue: 0 }

  state = { value: this.props.defaultValue }

  decrement = (): void => this.setState(({ value }) => ({ value: value - 1 }))

  increment = (): void => this.setState(({ value }) => ({ value: value + 1 }))

  badIncrement(): void {
    // `this` is not bound
    this.setState(({ value }) => ({ value: value + 1 }))
  }

  render = (): ReactNode => {
    const { value } = this.state

    return (
      <div>
        <button type="button" onClick={this.decrement}>
          -
        </button>
        <span> {value} </span>
        <button type="button" onClick={this.increment}>
          +
        </button>
      </div>
    )
  }
}

export default (
  <>
    <Counter />
    <Counter defaultValue={5} />
  </>
)
0
5

componentDidMount

  • after component rendered to DOM
  • use for:
    • data fetching
    • manipulating DOM

componentDidUpdate

  • after state or props update
  • usually a copy of componentDidMount

setState

  • triggers render
  • multiple calls are batched
  • always use setState to update state
  • this.state.something = 'bad'

componentWillUnmount

  • before removing component from DOM
  • use for:
    • freeing resources
    • canceling requests

Other lifecycle methods

Bitcoin Price

import React, { Component, ReactNode } from 'react'
import { BitcoinAPI } from './bitcoin-api'

export interface BitcoinProps {}

interface BitcoinState {
  price: number | null
  fiat: 'gbp' | 'usd'
}

export class Bitcoin extends Component<BitcoinProps, BitcoinState> {
  state = { price: null, fiat: 'gbp' } as const

  mounted = false

  componentDidMount = (): void => {
    this.mounted = true
    this.updatePrice()
  }

  componentDidUpdate = (
    prevProps: BitcoinProps,
    prevState: BitcoinState,
  ): void => {
    const { fiat } = this.state
    if (prevState.fiat !== fiat) {
      this.updatePrice()
    }
  }

  componentWillUnmount = (): void => {
    this.mounted = false
  }

  private setGbp = () => this.setState({ fiat: 'gbp' })

  private setUsd = () => this.setState({ fiat: 'usd' })

  private getSymbol = () => ({ gbp: '£', usd: '$' }[this.state.fiat])

  private updatePrice = async () => {
    this.setState({ price: null })

    const { fiat } = this.state
    const value = await BitcoinAPI.getPrice(fiat).catch(() => NaN)

    if (this.mounted) this.setState({ fiat, price: value })
  }

  render = (): ReactNode => {
    const { price } = this.state

    return (
      <div>
        <button type="button" onClick={this.setGbp}>
          £
        </button>
        <button type="button" onClick={this.setUsd}>
          $
        </button>
        <br />
        {price == null ? (
          <span>loading price...</span>
        ) : (
          <span>
            {this.getSymbol()}
            {price}
          </span>
        )}
      </div>
    )
  }
}

export default <Bitcoin />

loading price...
  • there is a subtle race-condition above; hard to resolve elegantly
  • cf. FC style below; shorter, simpler & race-condition free
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { BitcoinAPI } from './bitcoin-api'

export const Bitcoin: FC = () => {
  const [fiat, setFiat] = useState<'gbp' | 'usd'>('gbp')
  const setGbp = useCallback(() => setFiat('gbp'), [])
  const setUsd = useCallback(() => setFiat('usd'), [])

  const [price, setPrice] = useState<number | null>(null)
  useEffect(() => {
    let shouldUpdate = true

    setPrice(null)
    BitcoinAPI.getPrice(fiat)
      .then((v) => shouldUpdate && setPrice(v))
      .catch(() => shouldUpdate && setPrice(NaN))

    return () => {
      shouldUpdate = false
    }
  }, [fiat])

  const symbol = useMemo(() => ({ gbp: '£', usd: '$' }[fiat]), [fiat])

  return (
    <div>
      <button type="button" onClick={setGbp}>
        £
      </button>
      <button type="button" onClick={setUsd}>
        $
      </button>
      <br />
      {price == null ? (
        <span>loading price...</span>
      ) : (
        <span>
          {symbol}
          {price}
        </span>
      )}
    </div>
  )
}

export default <Bitcoin />

loading price...