Sophie Au

Software Developer, Web Designer, Tea Enthusiast

React Hooks: useState, useReducer and useEffect

03 May 2020

The three main hooks which you will use 99% of the time are useState, useEffect and useReducer. Although the latter is very rarely used too because it's complicated as hell. It's like Array.reduce which I hate but love at the same time too.

useState

Unlike props, React states are very fancy and capture, as the name says, the state of the component. This state is/should be inherent to the component and not changed from the ouside directly. A nice state would e.g. be a counter.

setState always replaces the old state completely. If you want to only update part of a state you need to do that explicitly.

const [state, setState] = useState(initialState)

setState(newState)

// to update the state partially (using a function)
setState(oldState => ({...oldState, key: newValue}))

If you have an expensive initial state you can also lazy-initialize the state with a function. Doing this will only calculate the inital state on the component initialization and not on every rerender.

// NOT this
const initialState = calcInitalState(props.value)
const [state, setState] = useState(initialState)

// but this
const [state, setState] = useState(() => calcInitalState(props.value))

useEffect

Always runs after every render/effect call. You can prevent that by adding in a dependency array. When you declare a dependency array, the effect will be called whenever a dependency updates. To have the effect run only once on initial render, put in an empty array (since the dependencies will never change since there are none). If you only want to conditionally run an effect when a dependency changes you need to add a guard inside the effect with e.g. an early return.

useEffect(() => {
    if(rerunEffectWhenThisChanges == "random value where we don't want to run the effect")
        return;

    doSomething()

    return () => runThisOnCleanup()
}, [rerunEffectWhenThisChanges])

// runs on every rerender
useEffect(() => {
    doSomething()

    return () => runThisOnCleanup()
})

// runs only once
useEffect(() => {
    doSomething()

    return () => runThisOnCleanup()
}, [])

If you're using multiple effects inside a single component, it might make sense do give them names by using the dreaded function declaration syntax. Not a fan, but it's super useful when debugging, because all of a sudden your effects show up with their names in the React Dev Tools:

useEffect(function fetchData() {});
useEffect(function updateCounter() {});

Also, async functions inside useEffects are a bit of a hassle. You can't just declare an effect as async so you need to define a function inside the hook which you then call. This is what it looks like:

useEffect(() => {
    const fetchSomething = async () => {
       //doing the async thing here
    }
    fetchSomething();
});

useReducer

Like I already said it's similar to Array.reduce in that it's super useful but I have no idea what the hell it's doing. And of course you can implement useState with useReducer.

const reducerFn = (state, updater) => {
    const newStateValue = doSomething()
    return newStateValue
}

const [state, updateStateFn] = useReducer(reducerFn, initialState)

// updateStateFn wraps reducerFn:
updateStateFn = (updater) => reducerFunc(currentState, updater)