Advanced Hooks

useCallback

  • const memoizedFn = useCallback(fn, deps)
  • <button onClick={() => ...} /> triggers unnecessary re-renders
  • because (() => ...) !== (() => ...)
  • every event handler should be wrapped with useCallback
  • docs

useMemo

  • const memoizedValue = useMemo(computeValueFn, deps)
  • useMemo will only recompute memoizedValue when one of deps have changed
  • wrap expensive computations to improve render performance
  • docs
import React, { FC, useMemo } from 'react'

const fib = (n: number): number => {
  if (n <= 2) return 1

  return fib(n - 1) + fib(n - 2)
}

export const Fib: FC<{ n: number }> = ({ n }) => {
  const f = useMemo(() => fib(n), [n])

  return (
    <pre>
      {n}-th fibonacci number: {f}
    </pre>
  )
}

export default <Fib n={10} />
10-th fibonacci number: 55

useReducer

  • const [currentState, dispatchFn] = useReducer(reducerFn, initialState)
  • use for
    • complex state
    • distributed state updates
    • migrating from redux
  • if possible, use multiple useStates instead
  • docs
import React, { FC, Reducer, useCallback, useReducer } from 'react'

interface State {
  count: number
}
interface IncrementAction {
  type: 'increment'
}
interface DecrementAction {
  type: 'decrement'
}
type Action = IncrementAction | DecrementAction

const reducer: Reducer<State, Action> = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      throw new Error()
  }
}

export const Counter: FC = () => {
  const [state, dispatch] = useReducer(reducer, { count: 0 })
  const decrement = useCallback(() => dispatch({ type: 'decrement' }), [])
  const increment = useCallback(() => dispatch({ type: 'increment' }), [])
  return (
    <>
      Count: {state.count}
      <button type="button" onClick={decrement}>
        -
      </button>
      <button type="button" onClick={increment}>
        +
      </button>
    </>
  )
}

export default <Counter />
Count: 0

useLayoutEffect

  • same as useEffect but runs synchronously, immediately after rendering
  • use for
    • DOM manipulation
    • when relying on order of effects
  • prefer useEffect
  • docs

useImperativeHandle

  • useImperativeHandle(ref, createRefValueFn, deps?)
  • use for
    • exposing an imperative API
    • syncing two refs
  • docs
import React, {
  FC,
  Ref,
  RefObject,
  useCallback,
  useImperativeHandle,
  useRef,
} from 'react'

export interface Focusable {
  focus(): void
}

export const FocusableInput: FC<{ focusable?: Ref<Focusable> }> = ({
  focusable,
}) => {
  const inputRef = useRef<HTMLInputElement>(null)
  const focus = useCallback(() => {
    inputRef.current?.focus()
  }, [])
  useImperativeHandle(focusable, () => ({ focus }), [focus])

  return (
    <div>
      <label>
        focusable input
        <input ref={inputRef} placeholder="required" />
      </label>
    </div>
  )
}

export const Error: FC<{ target: RefObject<Focusable> }> = ({
  target,
  children,
}) => {
  const onClick = useCallback(() => {
    target.current?.focus()
  }, [target])
  return (
    <div>
      {children}
      <button type="button" onClick={onClick}>
        focus field
      </button>
    </div>
  )
}

const Example: FC = () => {
  const focusableRef = useRef<Focusable>(null)

  return (
    <>
      <FocusableInput focusable={focusableRef} />
      <Error target={focusableRef}>this field is required</Error>
    </>
  )
}

export default <Example />
this field is required