til

today i learned

View on GitHub

Simplify Your Components with Finite State Machines and Derived States

Consider a common situation of a React component with multiple UI states such as loading, error, and success. The common paatern is to use multiple useState hooks to manage these states. This results in code that is hard to read and error-prone -

const MyComponent = () => {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(false)
  const [success, setSuccess] = useState(false)

  return (
    <div>
      {loading && !error && !success && <p>Loading...</p>}
      {error && !loading && !success && <p>Error occurred</p>}
      {success && !loading && !error && <p>Operation completed successfully</p>}
    </div>
  )
}

These states are distinct from each other. When loading is true, the error and success states should be false. Using multiple useState hooks can cause unexpected behaviors, like accidentally setting two states to true simultaneously.

Instead, consider using the “finite state machine” (FSM) pattern. A FSM allows only a finite number of states. In the UI example above, a single useState can manage the current state more robustly and with less risk of error, as shown here:

import { useState } from 'react'

type State = 'loading' | 'error' | 'success'

const MyComponent = () => {
  const [state, setState] = useState<State>('loading')

  const handleClick = () => {
    setState('loading')
    // Simulate an async operation
    setTimeout(() => {
      setState('success')
    }, 2000)
  }

  return (
    <div>
      {state === 'loading' && <p>Loading...</p>}
      {state === 'error' && <p>Error occurred</p>}
      {state === 'success' && <p>Operation completed successfully</p>}
      <button onClick={handleClick}>Click me</button>
    </div>
  )
}

An even better way would be to use something like Tanstack Query to fetch data. useQuery eliminates the need for separate useState hooks for loading, error, and success states.

source