educational0 min read

Using hooks

Intro

Hooks were introduced in React 16.8, which means prior to that, you had to reference each component lifecycle yourself, like componentDidMount, componentWillUpdate, etc. This is why you'll see these methods in older guides and tutorials, instead of Hooks.

You can use multiple of the same type of a hook in a given component, so you can have multiple useState, useEffect, etc.

Please note that hooks only work in functional components vs class components.

Photo of a fishing lure held over water

useState

useState does two things:

  • Sets a value for the component's initial state
  • Creates a function that allows you to update that value
const [blah, setBlah] = useState(0)
const [id, setId] = useState(arrayofItems[0])

In this example, because I'm passing in 0 as the initial value, it assumes blah is a number. If I try to do something like setBlah('test'), it will give me a TypeScript error:

Argument of type 'string' is not assignable to parameter of type 'SetStateAction<number>'.ts(2345)

There are several ways you can determine what gets passed in as your initial value, including arrayItems, functions, and more. It all depends on what type logic is driving the initial value for a given state.

useEffect

useEffect handles side effects and is used in scenarios where we want something to happen after a component renders. Previously, we would have used methods like componentDidMount and componentDidUpdate.

Typically you'll see examples using useEffect when fetching data or using setTimeout.

useEffect(() => {
  const timer = setTimeout(() => console.log('Hello, World!'), 3000)
  return () => clearTimeout(timer)
}, [])

In this example, we are creating a variable called timer which will be responsible for displaying "Hello world" in the console after 3000ms using setTimeout.

The second line clears our Timeout when the component unmounts, like willComponentUnmount. Notice that our second argument for useEffect is an empty array([]) which means it will only run once. If we passed in a value like [data] then clearTimeout would run every time data changes.

Side note: make sure you're returning a function for useEffect, otherwise you'll see this error:

destroy is not a function

useContext

Context is useful if you want to reference data from a parent or another component, where passing props is either messier or unavailable based on what you're trying to do.

What's nice is that as a hook, useContext makes this easy to read the context and subscribe to its changes.

const users = {
  primary: {
    name: 'Kirby',
    id: '001'
  },
  secondary: {
    name: 'Frodo',
    id: '002'
  }
}

const UserContext = createContext(users.primary)

function Greeting() {
  const user = useContext(UserContext)

  return (
    <>
      <h1>Welcome back, {user.name}!</h1>
      <span className="details">{user.id}</span>
    </>
  )
}

export default function App() {
  return (
    <UserContext.Provider value={users.secondary}>
      <Greeting />
    </UserContext.Provider>
  )
}

useCallback

A callback function is when you are passing a function into another function.

In the case that your component has multiple dependencies that can trigger a rerender, but you don't want the component to rerender any time they update, you'll want to memoize it. Memoization allows us to cache results and prevents unnecessary renders.

For example, if we want to worry about one function between renderings when clicking on a checkbox, we could do the following:

const [checked, setChecked] = useState(defaultChecked)
const [validationStatus, setValidationStatus] = useState(validationType)

const onChange = useCallback(
  (value?: boolean) => {
    setChecked(value)
    setValidationStatus(value ? '' : validationType)
  },
  [validationType]
)

useEffect(() => {
  onChange('', defaultChecked)
}, [defaultChecked, onChange])

By using useCallback, we are ensuring the our callbacks (setChecked and setValidationStatus) are memoized and not created on every re-render of the checkbox.

Our useEffect is saying that after the component renders, if defaultChecked or onChange update, trigger the onChange function.

useMemo

Speaking of memoization, let's talk about useMemo which looks similar to useCallback with this difference: useMemo returns a value when the component renders based on a dependency changing (instead of a memoized function).

This can be handy when you're wanting to avoid expensive calculations running on each render when a value is staying the same. Generally you'll want to use this for when you're generating a list of items, or data you don't expect to change.

You think of it like this: useCallback is for unnecessary renders, and useMemo for unnecessary calculations.

Note: useCallback(fn, deps) is the same as useMemo(() => fn, deps).

const List = useMemo(() => {
  return
    {...expensiveCalculations(a, b)}
}, [a, b]
});

useRef

Sometimes, you want to reference a variable that persists the full lifetime of a component. This is where useRef comes in handy, because you can take advantage of the .current property, which is passed to the initialValue.

A common use case is to have two HTML elements talk to each other like this:

export default function App() {
  const inputRef = useRef(null)
  const onClick = () => {
    inputRef.current.focus()
  }

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={onClick}>On click, focus the input</button>
    </>
  )
}

Wrapping up

One of my favorite things about Hooks is how we're able to use a combination of them to create performant, customizable experiences.

If you're ever stuck, try to console.log each part of the component lifecycle to identify what is being rerendered and go from there. Chances are you're forgetting to pass an argument or using the wrong type of hook to do what you need.

And if you have a cool custom Hook you've written, be sure to share it!