useState vs useReducer

<span class="text-highlight">disclaimer: this post is copy-paste heavy and is meant for my personal reference in the future</span>

References to the original authors:

  1. Dan Abramov's article on useEffect
  2. Robin Wieruch's article on data fetching with hooks

TLDR

useState = 1 output, 1 input useReducer = 1 output, multiple inputs (that can be grouped together to do one 'thing')

what is state?

state is something that stays when you function is run at different times. it's like your computer's RAM, of your function or class's short term memory.

you use it to keep track of stuff, like count, or time_since_last_accessed, or color_theme.

start with useState()

imagine this.setState({}) as something that updates the state that belongs to the component in a React class. useState() is something that updates a state of a variable.

this.setState({ count: this.state.count + 1 })
// not actualy the best way to update state, because it depends
// on something outside the setState function

is similar to

const [count, setCount] = useState(0)
// .... other code...
setCount(count + 1)

What is a reducer (pattern)?

it is a writing style (pattern) that allows you to group all your code's state changes into one.

if you are writing this.setState() or useState() multiple times to do one action (like fetch data), it's time to group them together with useReducer().

use useReducer() when you set a lot of states to do one 'thing'

when you are using useState() multiple times to do one 'thing'.

an example

lifted from here

useEffect(
  () => {
    const fetchData = async () => {
setIsError(false)
setIsLoading(true)
try { const result = await axios(url)
setData(result.data)
} catch (error) {
setIsError(true)
}
setIsLoading(false)
} fetchData() }, [url] )

in this case you are 'fetching data', but you need three states to account for loading UI and error. with the reducer pattern:

useEffect(
  () => {
    const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' })
try { const result = await axios(url)
dispatch({ type: 'FETCH_SUCCESS', payload: result.data })
} catch (error) {
dispatch({ type: 'FETCH_FAILURE' })
} } fetchData() }, [url] )

you're dispatching data (telling the reducer what you need) and then the reducer does the work for you, including

what does this data fetching reducer look like?

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false,
      }
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      }
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      }
    default:
      throw new Error()
  }
}

additional things to know

data won't be old if you dispatch from inside a useEffect(). It will be old if you use setSomething() in useEffect() without specifying it as a dependency:

const count = 0

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1)
  }, 1000)
  return () => clearInterval(id)
}, [])

^ count will never update

useEffect(
  () => {
    const id = setInterval(() => {
      setCount(count + 1)
    }, 1000)
    return () => clearInterval(id)
  },
  [count]
)

^ count will be updated, but setInterval and clearInterval will be called every render

###solution 1:

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1)
  }, 1000)
  return () => clearInterval(id)
}, [])

^ count takes its previous state, so it is correct

###solution 2:

but i need the reducer to depend on a prop!

function Counter({ step }) {
  const [count, dispatch] = useReducer(reducer, 0)

  function reducer(state, action) {
    if (action.type === 'tick') {
      return state + step
    } else {
      throw new Error()
    }
  }

  useEffect(
    () => {
      const id = setInterval(() => {
        dispatch({ type: 'tick' })
      }, 1000)
      return () => clearInterval(id)
    },
    [dispatch]
  )

  return <h1>{count}</h1>
}

This pattern disables a few optimizations so try not to use it everywhere, but you can totally access props from a reducer if you need to.

this is lifted from Dan Abramov's very lucid blog about react:

when you dispatch, React just remembers the action — but it will call your reducer during the next render. At that point the fresh props will be in scope, and you won’t be inside an effect.