DEV Community

Cover image for All React Hooks Explained
Siddharth Roy
Siddharth Roy

Posted on • Edited on • Originally published at siddharthroy.ml

All React Hooks Explained

Since React 16.8 the most common way to build a react component is using function because now we can have all the features of class components in functional components using hooks.

But why use a functional component instead of a class-based component?

Using a functional component with hooks reduces the line of codes and makes our code look more clean and readable.

In this blog, you are going to learn how to use the most used built-in react hooks and how to make a custom hook from scratch.

useState

const [state, setState] = useState(initialState)
Enter fullscreen mode Exit fullscreen mode

If you are used to class-based components you know that functional components don't state.

useState allows you to have state in functional components.

// const [value, setValue] = useState(initialValue)
const [name, setName] = useState('Siddharth')

console.log(name) // => Siddharth

setName('React') // sets the name to "React" and re-render the component

// Value of name after re-render

console.log(name) // => React
Enter fullscreen mode Exit fullscreen mode

The useState hook is a function like every other hook. It takes an initial value and returns an array containing the value and a function to change the value.

On first render the value is set to initialValue.

The setValue function is for updating the value. It takes the new value as the first argument and triggers a re-render on the component.

Here is an example to understand it better:

import { useState } from 'react'

function App() {
  console.log('Component render')
  const [number, setNumber] = useState(32)

  function updateNumber() {
    setNumber(Math.random())
  }

  return (<>
    <p>{ number }</p>
    <br />
    <button onClick={updateNumber}>Update number</button>
  </>)
}

export default App;
Enter fullscreen mode Exit fullscreen mode

App Demo gif

NOTE: It's doesn't have to be named as value and setValue, you can name it anything you want but value and setValue style is preferred among all developers.

If the new value is based on the previous value then you can do this:

const [number, setNumber] = useState(0)

 function updateNumber() {
   // Do this
   setNumber(prevNumber => prevNumber + 1)
   // not this
   setNumber(number + 1)
 }
Enter fullscreen mode Exit fullscreen mode

If you are storing an object inside a state then always use the object spread syntax to make a copy otherwise the component won't re-render.

const initialUserState = {
  name: 'Siddharth Roy',
  age: 17
}

const [user, setUser] = useState(initialUserState)

// Do this
setUser(prevState => {
  let newState = prevState
  newState.age = prevState.age + 1
  return {...prevState, ...newState} // Make a new copy using spread syntax
})
// After re-render user.age is 18


// Not this
setUser(prevState => {
  let newState = prevState
  newState.age = prevState.age + 1
  return newState
})
// Component won't re-render
Enter fullscreen mode Exit fullscreen mode

The reason behind this is React uses Object.is for comparing new value to previous value and if they are the same It won't re-render, and Object.is does not check what's inside the object.

let obj1 = { name: 's' }
let obj2 = { name: 's' }

Object.is(obj1, obj2) // => false

obj2 = obj1

Object.is(obj1, obj2) // => true

// Using spread operator to copy the object
obj2 = { ...obj1 }

Object.is(obj1, obj2) // => false
Enter fullscreen mode Exit fullscreen mode

NOTE: Spread operator won't copy nested objects, you will have to copy them manually.

useEffect

useEffect(didUpdate)
Enter fullscreen mode Exit fullscreen mode

The useEffect hook has many use cases, it is a combination of componentDidMountcomponentDidUpdate, and componentWillUnmount from Class Components.

Here is a simple demo of useEffect hook:

import { useState, useEffect } from 'react'

function App() {
  const [number, setNumber] = useState(0)

  useEffect(() => {
    console.log('This runs') // This will run when it mounts and update
  })

  return (<>
    <p>{ number }</p>
    <br />
    <button onClick={() => setNumber(prevNum => prevNum + 1)}>Increase Number</button>
  </>)
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The useEffect hook is a function that takes a function as its first argument and that function will run when the component mounts and update

As you saw the function ran the first time when the component got mounted and whenever it updated.

This function in the first argument of useEffect hook will only run when the component gets mounted and updated.

It also takes an array as a second optional argument and it behaves differently based on the array.

Like for this example, the function will run only run when the component mounts.

import { useState, useEffect } from 'react'

function App() {
  const [number, setNumber] = useState(0)

  useEffect(() => {
    console.log('Component Mounted') // Only runs when the component gets mounted
  }, []) // <-- Give an empty array in second argument

  return (<>
    <p>{ number }</p>
    <br />
    <button onClick={() => setNumber(prevNum => prevNum + 1)}>Increase Number</button>
  </>)
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The array we passed on in the second argument is called dependency list, when we omit the list the function run when the component mounts and when the component update (eg. When a state change), when we put an empty array in the second argument it only runs when the component gets mounted.

You can also put state inside the dependencies list and it will only run when the component gets mounted and when the state changes.

import { useState, useEffect } from 'react'

function App() {
  const [number, setNumber] = useState(0)
  const [message, setMessage] = useState('Hi')

  useEffect(() => {
    console.log('Component Mounted') // Only runs when the component gets mounted
  }, []) // <-- Give an empty array in second argument

  useEffect(() => {
    console.log('Component mounted or message changed')
  }, [message])

  useEffect(() => {
    console.log('Component mounted or number changed')
  }, [number])

  return (<>
    <p> { message} </p>
    <p>{ number }</p>
    <br />
    <button onClick={() => setMessage(prevMsg => prevMsg + 'i')}>Increase Hi</button>
    <button onClick={() => setNumber(prevNum => prevNum + 1)}>Increase Number</button>
  </>)
}

export default App;
Enter fullscreen mode Exit fullscreen mode

You can put multiple states in the dependency list but do note that if you are accessing any state from inside the function in useEffect hook then you have to put that state in the dependencies list.

useEffect(() => {
  // Do stuffs
}, [state1, state2, state3])

// Don't do this
useEffect(() => {
  // Doing something with state1
}, []) // <= Not providing state1 in dependencies list
Enter fullscreen mode Exit fullscreen mode

Now the last thing left is the cleanup function, this function is return by the function from the first argument and will run when the component gets unmounted.

useEffect(() => {
  // Initiate a request to API and update a state
  API.requestUserData()

  return () => { // Cleanup function
    // Cancel the request when the component gets unmounted
    API.cancelUserDataRequest()
  }
}, [])
Enter fullscreen mode Exit fullscreen mode

Sometimes when we run an async function when the comp gets mounted if the function tries to update a state after the comp gets unmounted it can cause memory leaks so it's better to stop that from happening using the cleanup function.

useContext

const value = useContext(MyContext)
Enter fullscreen mode Exit fullscreen mode

Normally if you want to share a state between components you would have to move the state to the uppermost component and then pass it down using props of every component. This method might be ok for small scale project but for a big scale project this can be tedious so to help with that useContext allow you to have a global state accessible from any component without passing down the state.

There are two functions to note when using Context API

// Create a context with a default value
const context = createContext(defaultValue) // defaultValue is optional

const value = useContext(conext) // Get the value from context
Enter fullscreen mode Exit fullscreen mode

Here is an example using Context API

In App.js:

import { useState, createContext } from 'react'
import Component1 from './Component1'
import Component2 from './Component2'
import Adder from './Adder'

const Context = createContext()

function App() {
  const [number, setNumber] = useState(0)

  return (<Context.Provider value={{number, setNumber}}>
    <p>Number: { number }</p>
    {/* Any component inside this component can access the value of the context */}
    {/* We can also provide the value of the context here */}

      <Component1> {/* Dummy component */}
        <Component2> {/* Dummy component */}
          <Adder />
        </Component2>
      </Component1>

  </Context.Provider>)
}

export { Context };
export default App;
Enter fullscreen mode Exit fullscreen mode

In Adder.js:

import { useContext } from 'react'
import { Context } from './App'

export default function Adder() {
    const contextValue = useContext(Context)

    return (<div style={{border: '1px solid black'}}>
        <p>Inside Adder Component</p>
        <p>Number: { contextValue.number }</p>
        <button onClick={() => contextValue.setNumber(prevNum => prevNum + 1)}>Add Number</button>
    </div>)
}
Enter fullscreen mode Exit fullscreen mode

The result:

Explanation

  • In App.js we are creating a context and using the Provider Component inside the Context object returned by createContext as the uppermost component. Any component inside Context.Provider Component can access the value of the Context
  • We are also passing the number and setNumber from App.js as the value of the Context using the value prop of the Context.Provider component
  • We need to export this Context object to be used inside the other components when using useContext
  • In Adder.js we are simply importing the Context object and using it with useContext hook to get the value of the context
  • The object returned by useContext contains the value we provided in the value prop of the provider component

Note that whenever the value of context change the entire component tree gets re-rendered and can affect performance. If you don't want that behavior it's better to use external solutions for global state management like react-redux that only re-render the desired component.

You can also have multiple context and context providers if you want.

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init)
Enter fullscreen mode Exit fullscreen mode

This is an alternative to useState, it takes an additional function called reducer, it's similar to how redux handles state.

useReducer is useful when you have a complex state, like an object with multiple sub-values.

Here is a simple counter example from React Docs using useReducer:

import { useReducer } from 'react'

const initialState = {count: 0}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1}
    case 'decrement':
      return {count: state.count - 1}
    default:
      throw new Error()
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Here is another example using complex state:

import { useReducer } from 'react'

const initialState = {
  username: 'Siddharth_Roy12',
  age: 17,
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment_age':
      return {...state, age: state.age + 1}
    case 'decrement_age':
      return {...state, age: state.age - 1}
    case 'change_username':
      return {...state, username: action.payload}
    default:
      throw new Error();
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <>
      <p>Username: { state.username }</p>
      <p>Age: { state.age }</p>

      <button onClick={() => dispatch({type: 'decrement_age'})}>-</button>
      <button onClick={() => dispatch({type: 'increment_age'})}>+</button>
      <input
        type="text"
        value={state.username}
        onChange={(e) => dispatch({
          type: 'change_username',
          payload: e.target.value
        })}
      />
    </>
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Lazy initialization

You can also create the initial state lazily. To do this, you can pass an init function as the third argument. The initial state will be set to init(initialArg).

It lets you extract the logic for calculating the initial state outside the reducer. This is also handy for resetting the state later in response to an action:

import { useReducer } from 'react'

const initialCount = 0

function init(initialCount) {
  return {count: initialCount};
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1}
    case 'decrement':
      return {count: state.count - 1}
    default:
      throw new Error()
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialCount, init)
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
Enter fullscreen mode Exit fullscreen mode

Usually, if you have an inline function in a react component, whenever that component re-render that function will also get re-created

The useCallback hook takes an inline function and a dependencies list and returns a memoized version of that function. That function will only recreate when its dependencies change.

You can visualize the function re-creation using a Set

NOTE: Set can only have unique elements. Object.is is used to check and remove duplicate elements.

Without useCallback:

import { useState } from 'react'

const functionsCounter = new Set()

function App() {
  const [count, setCount] = useState(0)
  const [otherCounter, setOtherCounter] = useState(0)

  const increment = () => {
    setCount(count + 1)
  }
  const decrement = () => {
    setCount(count - 1)
  }
  const incrementOtherCounter = () => {
    setOtherCounter(otherCounter + 1)
  }

  functionsCounter.add(increment)
  functionsCounter.add(decrement)
  functionsCounter.add(incrementOtherCounter)

  console.log(functionsCounter.size)

  return (
    <>
      Count: {count}
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={incrementOtherCounter}>incrementOtherCounter</button>
    </>
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode

With useCallback:

import { useState, useCallback } from 'react'

const functionsCounter = new Set()

function App() {
  const [count, setCount] = useState(0)
  const [otherCounter, setOtherCounter] = useState(0)

  const increment = useCallback(() => {
    setCount(count + 1)
  }, [count])
  const decrement = useCallback(() => {
    setCount(count - 1)
  }, [count])
  const incrementOtherCounter = useCallback(() => {
    setOtherCounter(otherCounter + 1)
  }, [otherCounter])


  functionsCounter.add(increment)
  functionsCounter.add(decrement)
  functionsCounter.add(incrementOtherCounter)

  console.log(functionsCounter.size)

  return (
    <>
      Count: {count}
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={incrementOtherCounter}>incrementOtherCounter</button>
    </>
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The use cases of the hook are very small, you will most likely never have to use this hook.

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Enter fullscreen mode Exit fullscreen mode

The useMemo hooks take a function to compute a value and a dependency array and return a memoized value. This will only re-compute the value when its dependencies have changed.

This hook is useful when you are doing expensive calculations inside a component every time it renders.

An example without useMemo:

function DemoComponent() {
  const [state1, setState1] = useState(3)
  const [state2, setState2] = useState(Math.PI)

  const someValue = computeExpensiveValue(state1, state2) // Takes 0.6ms on every render

  return (<>
    { someValue }
  </>)
}
Enter fullscreen mode Exit fullscreen mode

With useMemo:

function DemoComponent() {
  const [state1, setState1] = useState(3)
  const [state2, setState2] = useState(Math.PI)

  const someValue = useMemo(() => {
    return computeExpensiveValue(state1, state2) // This only runs when the state1 or state2 changes
  }, [state1, state2])

  return (<>
    { someValue }
  </>)
}
Enter fullscreen mode Exit fullscreen mode

useRef

const refContainer = useRef(initialValue)
Enter fullscreen mode Exit fullscreen mode

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

The most common use case of this hook is to store a reference to a DOM Element.

function TextInputWithFocusButton() {
  const inputEl = useRef(null)
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus()
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Another use case is to store a mutable value and it will persist during the entire life cycle of the component, but do note that whenever you change the .current property the component won't re-render.

Custom hook from scratch

Now that you have learned how to use all react hooks It's time to build your own hook from scratch.

A custom hook is just a regular javascript function that uses the other hooks provided by React to extract component logic into a reusable function.

For example, look at this component

function App() {
  const mounted = useRef(false)

  useEffect(() => { // To check if component is mounted or not
        mounted.current = true

        return () => { 
            mounted.current = false
        }
    }, [])

  // To check if the component is mounted or not check mounted.current
  if (mounted.current) {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

This component uses two hooks to check if the component is mounted or not. This is useful when you are running a long async function and the component can dismount at any time.

We can extract this logic into a reusable function.

function useIsMounted() { // React hook name must start from use
  const mounted = useRef(false)

  useEffect(() => {
        mounted.current = true

        return () => { 
            mounted.current = false
        }
    }, [])

  return () => mounted.current
}
Enter fullscreen mode Exit fullscreen mode

Then use it like this

function App() {
  const isMounted = useIsMounted()

  // To check if is mounted
  if (isMounted()) {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Now our code looks more cleaner and we can use the same logic in many components.

Top comments (0)