DEV Community

Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

React Hooks cheat sheet: Unlock solutions to common problems

While Hooks may have been the talk of the community for a while now, beyond the fuss, they do have a very simple API.

This article will highlight examples and use cases, from simple to advanced. I’ve also built an accompanying web app for live interaction with the examples herein.

Please note that this article includes a lot of code snippets and assumes some Hooks fluency. You may want to start here if you’re completely new to Hooks.

Let’s get started.

useState

useState lets you use local state within a function component. You can view the docs for it here and view a live, editable cheat sheet here.

Declare state variable

Declaring a state variable is as simple as calling useState with some initial state value.

useState(initialStateValue)
const DeclareStateVar = () => {
  const [count] = useState(100)
  return <div> State variable is {count}</div>
}

Simple usage of the useState hook.

Update state variable

Updating a state variable is as simple as invoking the updater function returned by the useState invocation.

const [stateValue, updaterFn] = useState (initialStateValue);

Note how the age state variable is being updated.

const UpdateStateVar = () => {
  const [age, setAge] = useState(19)
  const handleClick = () => setAge(age + 1)

  return (
    <div>
      Today I am {age} Years of Age
      <div>
        <button onClick={handleClick}>Get older! </button>
      </div>
    </div>
  )
}

Here's the code responsible for the screencast above.

Multiple state variables

Multiple state variables may be used and updated from within a functional component, as shown below:


const MultipleStateVars = () => {
  const [age, setAge] = useState(19)
  const [siblingsNum, setSiblingsNum] = 
    useState(10)

  const handleAge = () => setAge(age + 1)
  const handleSiblingsNum = () => 
      setSiblingsNum(siblingsNum + 1)


  return (
    <div>
      <p>Today I am {age} Years of Age</p>
      <p>I have {siblingsNum} siblings</p>

      <div>
        <button onClick={handleAge}>
          Get older! 
        </button>
        <button onClick={handleSiblingsNum}>
            More siblings! 
        </button>
      </div>
    </div>
  )
}

Here's the code responsible for the screencast above.

Use object state variable

As opposed to strings and numbers, you could also use an object as the initial value passed to useState.

Note that you have to pass the entire object to the useState updater function because the object is replaced, not merged.

// 🐢 setState (object merge) vs useState (object replace)
// assume initial state is {name: "Ohans"}

setState({ age: 'unknown' })
// new state object will be
// {name: "Ohans", age: "unknown"}

useStateUpdater({ age: 'unknown' })
// new state object will be
// {age: "unknown"} - initial object is replaced

setState vs. useState updater function.

Multiple state objects updated via a state object variable.

const StateObject = () => {
  const [state, setState] = useState({ age: 19, siblingsNum: 4 })
  const handleClick = val =>
    setState({
      ...state,
      [val]: state[val] + 1
    })
  const { age, siblingsNum } = state

  return (
    <div>
      <p>Today I am {age} Years of Age</p>
      <p>I have {siblingsNum} siblings</p>

      <div>
        <button onClick={handleClick.bind(null, 'age')}>Get older!</button>
        <button onClick={handleClick.bind(null, 'siblingsNum')}>
          More siblings!
        </button>
      </div>
    </div>
  )
}

Here's the code for the screencast above.

Initialize state from function

As opposed to just passing an initial state value, state could also be initialized from a function, as shown below:

const StateFromFn = () => {
  const [token] = useState(() => {
    let token = window.localStorage.getItem("my-token");
    return token || "default#-token#"
  })

  return <div>Token is {token}</div>
}

Functional setState

The updater function returned from invoking useState can also take a function similar to the good ol’ setState:

const [value, updateValue] = useState(0)
// both forms of invoking "updateValue" below are valid 👇
updateValue(1);
updateValue(previousValue => previousValue + 1);

This is ideal when the state update depends on some previous value of state.

A counter with functional setState updates.

const CounterFnSetState = () => {
  const [count, setCount] = useState(0);
  return (
    <>
      <p>Count value is: {count}</p>
      <button onClick={() => setCount(0)}>Reset</button>
      <button 
        onClick={() => setCount(prevCount => prevCount + 1)}>
        Plus (+)
      </button>
      <button 
        onClick={() => setCount(prevCount => prevCount - 1)}>
       Minus (-)
      </button>
    </>
  );
}

Here's the code for the screencast above.

useEffect

useEffect accepts a function, which can perform any side effects. View the docs here, and check out the live, editable cheat sheet.

Basic side effect

Watch the title of the document update.

const BasicEffect = () => {
  const [age, setAge] = useState(0)
  const handleClick = () => setAge(age + 1)

  useEffect(() => {
    document.title = 'You are ' + age + ' years old!'
  })

  return <div>
    <p> Look at the title of the current tab in your browser </p>
    <button onClick={handleClick}>Update Title!! </button>
  </div>
}

Here's the code responsible for the screencast above.

Effect with cleanup

It’s pretty common to clean up an effect after some time. This is possible by returning a function from within the effect function passed to useEffect. Below is an example with addEventListener.

const EffectCleanup = () => {
  useEffect(() => {
    const clicked = () => console.log('window clicked')
    window.addEventListener('click', clicked)

    // return a clean-up function
    return () => {
      window.removeEventListener('click', clicked)
    }
  }, [])

  return <div>
    When you click the window you'll 
    find a message logged to the console
  </div>
}

Multiple effects

Multiple useEffect calls can happen within a functional component, as shown below:

const MultipleEffects = () => {
  // 🍟
  useEffect(() => {
    const clicked = () => console.log('window clicked')
    window.addEventListener('click', clicked)

    return () => {
      window.removeEventListener('click', clicked)
    }
  }, [])

  // 🍟 another useEffect hook 
  useEffect(() => {
    console.log("another useEffect call");
  })

  return <div>
    Check your console logs
  </div>
}

Note thatuseEffect calls can be skipped — i.e., not invoked on every render. This is done by passing a second array argument to the effect function.

Skipping effects (array dependency)

const ArrayDep = () => {
  const [randomNumber, setRandomNumber] = useState(0)
  const [effectLogs, setEffectLogs] = useState([])

  useEffect(
    () => {
      setEffectLogs(prevEffectLogs => [...prevEffectLogs, 'effect fn has been invoked'])
    },
    [randomNumber]
  )

  return (
    <div>
      <h1>{randomNumber}</h1>
      <button
        onClick={() => {
          setRandomNumber(Math.random())
        }}
      >
        Generate random number!
      </button>
      <div>
        {effectLogs.map((effect, index) => (
          <div key={index}>{'🍔'.repeat(index) + effect}</div>
        ))}
      </div>
    </div>
  )
}

In the example above, useEffect is passed an array of one value: [randomNumber].

Thus, the effect function will be called on mount and whenever a new random number is generated.

Here’s the “Generate random number” button being clicked and the effect function being rerun upon generating a new random number:

Skipping effects (empty array dependency )

In this example, useEffect is passed an empty array, []. Therefore, the effect function will be called only on mount.

const ArrayDepMount = () => {
  const [randomNumber, setRandomNumber] = useState(0)
  const [effectLogs, setEffectLogs] = useState([])

  useEffect(
    () => {
      setEffectLogs(prevEffectLogs => [...prevEffectLogs, 'effect fn has been invoked'])
    },
    []
  )

  return (
    <div>
      <h1>{randomNumber}</h1>
      <button
        onClick={() => {
          setRandomNumber(Math.random())
        }}
      >
        Generate random number!
      </button>
      <div>
        {effectLogs.map((effect, index) => (
          <div key={index}>{'🍔'.repeat(index) + effect}</div>
        ))}
      </div>
    </div>
  )
}

Here’s the button being clicked and the effect function not invoked:

Skipping effects (no array dependency)

Without an array dependency, the effect function will be run after every single render.

useEffect(() => {
console.log(This will be logged after every render!)
})

useContext

useContext saves you the stress of having to rely on a Context consumer. It has a simpler API when compared to MyContext.Consumer and the render props API it exposes. View the docs here, and view a live, editable cheat sheet.

The following example highlights the difference between consuming a context object value via useContext or Context.Consumer:

// example Context object
const ThemeContext = React.createContext("dark");

// usage with context Consumer
function Button() {
  return <ThemeContext.Consumer>
        {theme => <button className={theme}> Amazing button </button>}
  </ThemeContext.Consumer>
}


// usage with useContext hook 
import {useContext} from 'react';

function ButtonHooks() {
 const theme = useContext(ThemeContext)
 return <button className={theme}>Amazing button</button>
} 

Here’s a live example with useContext:


const ThemeContext = React.createContext('light');

const Display = () => {
 const theme = useContext(ThemeContext);
 return <div
        style={{
        background: theme === 'dark' ? 'black' : 'papayawhip',
        color: theme === 'dark' ? 'white' : 'palevioletred',
        width: '100%',
        minHeight: '200px'
        }}
    >
        {'The theme here is ' + theme}
    </div>
}

Here's the code responsible for the example above.

useLayoutEffect

useLayoutEffect has the very same signature as useEffect. We’ll discuss the difference between useLayoutEffect and useEffect below. Again, view the docs and the live, editable cheat sheet.

useLayoutEffect(() => {
//do something
}, [arrayDependency])

Similar usage as useEffect

Here’s the same example for useEffect built with useLayoutEffect:


const ArrayDep = () => {
    const [randomNumber, setRandomNumber] = useState(0)
    const [effectLogs, setEffectLogs] = useState([])

    useLayoutEffect(
      () => {
        setEffectLogs(prevEffectLogs => [...prevEffectLogs, 'effect fn has been invoked'])
      },
      [randomNumber]
    )

    return (
      <div>
        <h1>{randomNumber}</h1>
        <button
          onClick={() => {
            setRandomNumber(Math.random())
          }}
        >
          Generate random number!
        </button>
        <div>
          {effectLogs.map((effect, index) => (
            <div key={index}>{'🍔'.repeat(index) + effect}</div>
          ))}
        </div>
      </div>
    )
  }

Here's the code responsible for the screencast above.

useLayoutEffect vs. useEffect

The function passed to useEffect fires after layout and paint, i.e., after the render has been committed to the screen. This is OK for most side effects that shouldn’t block the browser from updating the screen.

There are cases where you may not want the behavior useEffect provides, though; for example, if you need to make a visual change to the DOM as a side effect, useEffect won’t be the best choice.

To prevent the user from seeing flickers of changes, you can use useLayoutEffect. The function passed to useLayoutEffect will be run before the browser updates the screen.

useReducer

useReducer may be used as an alternative to useState. It’s ideal for complex state logic where there’s a dependency on previous state values or a lot of state sub-values.

Depending on your use case, you may find useReducer quite testable. View the docs and the live, editable cheat sheet.

Basic usage

As opposed to calling useState, call useReducer with a reducer and initialState, as shown below. The useReducer call returns the state property and a dispatch function.

Increase/decrease bar size by managing state with useReducer

const initialState = { width: 15 };

const reducer = (state, action) => {
  switch (action) {
    case 'plus':
      return { width: state.width + 15 }
    case 'minus':
      return { width: Math.max(state.width - 15, 2) }
    default:
      throw new Error("what's going on?" )
  }
}

const Bar = () => {
  const [state, dispatch] = useReducer(reducer, initialState)
  return <>
    <div style={{ background: 'teal', height: '30px', width: state.width }}></div>
    <div style={{marginTop: '3rem'}}>
        <button onClick={() => dispatch('plus')}>Increase bar size</button>
        <button onClick={() => dispatch('minus')}>Decrease bar size</button>
    </div>
    </>
}

ReactDOM.render(<Bar />)

Here's the code responsible for the screencast above.

Initialize state lazily

useReducer takes a third function parameter. You may initialize state from this function, and whatever’s returned from this function is returned as the state object. This function will be called with initialState — the second parameter.

Same increase/decrease bar size — with state initialized lazily.

const initializeState = () => ({
  width: 100
})

// ✅ note how the value returned from the fn above overrides initialState below: 
const initialState = { width: 15 }
const reducer = (state, action) => {
  switch (action) {
    case 'plus':
      return { width: state.width + 15 }
    case 'minus':
      return { width: Math.max(state.width - 15, 2) }
    default:
      throw new Error("what's going on?" )
  }
}

const Bar = () => {
  const [state, dispatch] = useReducer(reducer, initialState, initializeState)
  return <>
    <div style={{ background: 'teal', height: '30px', width: state.width }}></div>
    <div style={{marginTop: '3rem'}}>
        <button onClick={() => dispatch('plus')}>Increase bar size</button>
        <button onClick={() => dispatch('minus')}>Decrease bar size</button>
    </div>
    </>
}

ReactDOM.render(Bar)

Here's the code responsible for the screencast above.

Imitate this.setState’s behavior

useReducer uses a reducer that isn’t as strict as Redux’s. For example, the second parameter passed to the reducer, action, doesn’t need to have a type property.

This allows for interesting manipulations, such as renaming the second parameter and doing the following:

const initialState = { width: 15 }; 

const reducer = (state, newState) => ({
  ...state,
  width: newState.width
})

const Bar = () => {
  const [state, setState] = useReducer(reducer, initialState)
  return <>
    <div style={{ background: 'teal', height: '30px', width: state.width }}></div>
    <div style={{marginTop: '3rem'}}>
        <button onClick={() => setState({width: 100})}>Increase bar size</button>
        <button onClick={() => setState({width: 3})}>Decrease bar size</button>
    </div>
    </>
}

ReactDOM.render(Bar)

The results remain the same with a setState like api imitated.

useCallback

useCallback returns a memoized callback. View the docs and the view live, editable cheat sheet here.

Starter example

The following example will form the basis of the explanations and code snippets that follow.


const App = () => {
    const [age, setAge] = useState(99)
    const handleClick = () => setAge(age + 1)
    const someValue = "someValue"
    const doSomething = () => {
      return someValue
    }

    return (
      <div>
        <Age age={age} handleClick={handleClick}/>
        <Instructions doSomething={doSomething} />
      </div>
    )
}

const Age = ({ age, handleClick }) => {
  return (
    <div>
      <div style={{ border: '2px', background: "papayawhip", padding: "1rem" }}>
        Today I am {age} Years of Age
      </div>
      <pre> - click the button below 👇 </pre>
      <button onClick={handleClick}>Get older! </button>
    </div>
  )
}

const Instructions = React.memo((props) => {
  return (
    <div style={{ background: 'black', color: 'yellow', padding: "1rem" }}>
      <p>Follow the instructions above as closely as possible</p>
    </div>
  )
})

ReactDOM.render (
  <App />
)

In the example above, the parent component, , is updated (and re-rendered) whenever the “Get older” button is clicked.

Consequently, the child component is also re-rendered because the doSomething prop is passed a new callback with a new reference.

Note that even though the Instructions child component uses React.memo to optimize performace, it is still re-rendered.

How can this be fixed to prevent from re-rendering needlessly?

useCallback with referenced function

const App = () => {
  const [age, setAge] = useState(99)
  const handleClick = () => setAge(age + 1)
  const someValue = "someValue"
  const doSomething = useCallback(() => {
    return someValue
  }, [someValue])

  return (
    <div>
      <Age age={age} handleClick={handleClick} />
      <Instructions doSomething={doSomething} />
    </div>
  )
}

const Age = ({ age, handleClick }) => {
  return (
    <div>
      <div style={{ border: '2px', background: "papayawhip", padding: "1rem" }}>
        Today I am {age} Years of Age
      </div>
      <pre> - click the button below 👇 </pre>
      <button onClick={handleClick}>Get older! </button>
    </div>
  )
}

const Instructions = React.memo((props) => {
  return (
    <div style={{ background: 'black', color: 'yellow', padding: "1rem" }}>
      <p>Follow the instructions above as closely as possible</p>
    </div>
  )
})

ReactDOM.render(<App />)

useCallback with inline function

useCallback also works with an inline function as well. Here’s the same solution with an inline useCallback call:

const App = () => {
  const [age, setAge] = useState(99)
  const handleClick = () => setAge(age + 1)
  const someValue = "someValue"

  return (
    <div>
      <Age age={age} handleClick={handleClick} />
      <Instructions doSomething={useCallback(() => {
        return someValue
      }, [someValue])} />
    </div>
  )
}

const Age = ({ age, handleClick }) => {
  return (
    <div>
      <div style={{ border: '2px', background: "papayawhip", padding: "1rem" }}>
        Today I am {age} Years of Age
      </div>
      <pre> - click the button below 👇 </pre>
      <button onClick={handleClick}>Get older! </button>
    </div>
  )
}

const Instructions = memo((props) => {
  return (
    <div style={{ background: 'black', color: 'yellow', padding: "1rem" }}>
      <p>Follow the instructions above as closely as possible</p>
    </div>
  )
})

render(<App />)

useMemo

useMemo returns a memoized value. View the docs and the live, editable cheat sheet.

Starter example

The following example will form the basis of the explanations and code snippets that follow.


const App = () => {
    const [age, setAge] = useState(99)
    const handleClick = () => setAge(age + 1)
    const someValue = { value: "someValue" }
    const doSomething = () => {
      return someValue
    }

    return (
      <div>
        <Age age={age} handleClick={handleClick}/>
        <Instructions doSomething={doSomething} />
      </div>
    )
}

const Age = ({ age, handleClick }) => {
  return (
    <div>
      <div style={{ border: '2px', background: "papayawhip", padding: "1rem" }}>
        Today I am {age} Years of Age
      </div>
      <pre> - click the button below 👇 </pre>
      <button onClick={handleClick}>Get older! </button>
    </div>
  )
}

const Instructions = React.memo((props) => {
  return (
    <div style={{ background: 'black', color: 'yellow', padding: "1rem" }}>
      <p>Follow the instructions above as closely as possible</p>
    </div>
  )
})

ReactDOM.render (
  <App />
)

Here's the code responsible for the screencast above.

The example above is similar to the one foruseCallback. The only difference here is that someValue is an object, not a string. Owing to this, the Instructions component still re-renders despite the use of React.memo

Why? Objects are compared by reference, and the reference to someValue changes whenever re-renders.

Any solutions?

Basic usage

The object someValue may be memoized using useMemo. This prevents the needless re-render.

const App = () => {
    const [age, setAge] = useState(99)
    const handleClick = () => setAge(age + 1)
    const someValue = useMemo(() => ({ value: "someValue" }))
    const doSomething = () => {
      return someValue
    }

    return (
      <div>
        <Age age={age} handleClick={handleClick}/>
        <Instructions doSomething={doSomething} />
      </div>
    )
}

const Age = ({ age, handleClick }) => {
  return (
    <div>
      <div style={{ border: '2px', background: "papayawhip", padding: "1rem" }}>
        Today I am {age} Years of Age
      </div>
      <pre> - click the button below 👇 </pre>
      <button onClick={handleClick}>Get older! </button>
    </div>
  )
}

const Instructions = React.memo((props) => {
  return (
    <div style={{ background: 'black', color: 'yellow', padding: "1rem" }}>
      <p>Follow the instructions above as closely as possible</p>
    </div>
  )
})

ReactDOM.render (<App />)

useRef

useRef returns a “ref” object. Values are accessed from the .current property of the returned object. The .current property could be initialized to an initial value — useRef(initialValue), for example. The object is persisted for the entire lifetime of the component. Reference the docs and the live, editable cheat sheet.

Accessing the DOM

Consider the sample application below:

Accessing the DOM via useRef.

const AccessDOM = () => {
  const textAreaEl = useRef(null);
  const handleBtnClick = () => {
    textAreaEl.current.value =
    "The is the story of your life. You are an human being, and you're on a website about React Hooks";
    textAreaEl.current.focus();
  };
  return (
    <section style={{ textAlign: "center" }}>
      <div>
        <button onClick={handleBtnClick}>Focus and Populate Text Field</button>
      </div>
      <label
        htmlFor="story"
        style={{
          display: "block",
          background: "olive",
          margin: "1em",
          padding: "1em"
        }}
      >
        The input box below will be focused and populated with some text
        (imperatively) upon clicking the button above.
      </label>
      <textarea ref={textAreaEl} id="story" rows="5" cols="33" />
    </section>
  );
};

Here's the code responsible for the screencast above.

Instance-like variables (generic container)

Other than just holding DOM refs, the “ref” object can hold any value. Consider a similar application below, where the ref object holds a string value:


const HoldStringVal = () => {
    const textAreaEl = useRef(null);
    const stringVal = useRef("This is a string saved via the ref object --- ")
    const handleBtnClick = () => {
      textAreaEl.current.value =
      stringVal.current + "The is the story of your life. You are an human being, and you're on a website about React Hooks";
      textAreaEl.current.focus();
    };
    return (
      <section style={{ textAlign: "center" }}>
        <div>
          <button onClick={handleBtnClick}>Focus and Populate Text Field</button>
        </div>
        <label
          htmlFor="story"
          style={{
            display: "block",
            background: "olive",
            margin: "1em",
            padding: "1em"
          }}
        >
          Prepare to see text from the ref object here. Click button above.
        </label>
        <textarea ref={textAreaEl} id="story" rows="5" cols="33" />
      </section>
    );
  };

Here's the code responsible for the screencast above.

You could do the same as storing the return value from a setInterval for cleanup.

function TimerWithRefID() {
  const setIntervalRef = useRef();

  useEffect(() => {
    const intervalID = setInterval(() => {
      // something to be done every 100ms
    }, 100);

    // this is where the interval ID is saved in the ref object 
    setIntervalRef.current = intervalID;
    return () => {
      clearInterval(setIntervalRef.current);
    };
  });
}

Other examples

Working on a near-real-world example can help bring your knowledge of Hooks to life. Until data fetching with React Suspense is released, fetching data via Hooks proves to be a good exercise for more Hooks practice.

Below’s an example of fetching data with a loading indicator:


const fetchData = () => {
  const stringifyData = data => JSON.stringify(data, null, 2)
  const initialData = stringifyData({ data: null })
  const loadingData = stringifyData({ data: 'loading...' })
  const [data, setData] = useState(initialData)

  const [gender, setGender] = useState('female')
  const [loading, setLoading] = useState(false)

  useEffect(
    () => {
      const fetchData = () => {
        setLoading(true)
        const uri = 'https://randomuser.me/api/?gender=' + gender
        fetch(uri)
          .then(res => res.json())
          .then(({ results }) => {
            setLoading(false)
            const { name, gender, dob } = results[0]
            const dataVal = stringifyData({
              ...name,
              gender,
              age: dob.age
            })
            setData(dataVal)
          })
      }

      fetchData()
    },
    [gender]
  )

  return (
    <>
      <button
        onClick={() => setGender('male')}
        style={{ outline: gender === 'male' ? '1px solid' : 0 }}
      >
        Fetch Male User
      </button>
      <button
        onClick={() => setGender('female')}
        style={{ outline: gender === 'female' ? '1px solid' : 0 }}
      >
        Fetch Female User
      </button>

      <section>
        {loading ? <pre>{loadingData}</pre> : <pre>{data}</pre>}
      </section>
    </>
  )
}

Here's the code responsible for the screencast above.

Conclusion

Hooks give a lot of power to functional components. I hope this cheat sheetproves useful in your day-to-day use of Hooks. Cheers!

Thanks to Hooks and a couple other new React features. Illustration by me :)


Plug: LogRocket, a DVR for web apps

https://logrocket.com/signup/

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

Try it for free.


The post React Hooks cheat sheet: Unlock solutions to common problems appeared first on LogRocket Blog.

Top comments (0)