banner
 Sayyiku

Sayyiku

Chaos is a ladder
telegram
twitter

03-React-Hook

React Hook#

React Hook is a new feature introduced in React 16.8. It allows you to use state and other React features without writing a class.

Embracing React Hook#

What is a Hook?#

A Hook is a function that lets you “hook into” React state and lifecycle features from function components. Hooks cannot be used in class components.

When to use Hooks?#

If you are writing a function component and realize you need to add some state, the previous approach required converting it to a class, but now you can use Hooks in your existing function components.

State Hook#

The State Hook allows you to add state to React function components. In a class, you can initialize state by setting this.state in the constructor, but in function components, we don’t have this, so we cannot assign or read this.state. Instead, we call useState directly in the component. For example:

import React, { useState } from 'react'

export default function Hello(prop) {
  const [name, setName] = useState('chanshiyu')
  const handleChange = e => setName(e.target.value)

  return (
    <div>
      <Input placeholder="Your name" value={name} onChange={handleChange} />
    </div>
  )
}

useState is a new method provided by React, which is a way to preserve variables across function calls, and it provides the same functionality as this.state in classes. Generally, variables will "disappear" after the function exits, but variables in state will be retained by React.

The only parameter for the useState method is the initial state. Unlike class initial state, which must be an object, the parameter for useState can be of any type, such as a number or string, and does not have to be an object. If the initial state needs to be computed through complex calculations, you can pass a function that computes and returns the initial state, which will only be called during the initial render.

Calling useState returns the current state and a function to update the state, which can be accessed via array destructuring. Unlike this.setState in classes, updating the state variable always replaces it rather than merging it.

Of course, if there are multiple form fields, the best implementation is to extract the Hook into a reusable function:

import React, { useState } from 'react'

export default function Hello(prop) {
  const name = useFormInput('chanshiyu')
  const age = useFormInput('24')

  return (
    <div>
      <Input placeholder="Your name" value={name.value} onChange={name.onChange} />
      <Input placeholder="Your age" value={age.value} onChange={age.onChange} />
    </div>
  )
}

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue)
  const handleChange = e => setValue(e.target.value)

  return {
    value,
    onChange: handleChange
  }
}

If calculating the initial value is expensive, you can pass a function, which will only execute once:

function Table(props) {
  // ⚠️ createRows() is called on every render
  const [rows, setRows] = useState(createRows(props.count))

  // ✅ createRows() is only called once
  const [rows, setRows] = useState(() => createRows(props.count))
}

Effect Hook#

The Effect Hook allows you to perform side effects in function components. Data fetching, setting up subscriptions, and manually changing the DOM in React components are all considered side effects. Common side effects in React components can generally be divided into two types: those that do not need cleanup and those that do.

Effects that do not need cleanup#

Here’s an example of a side effect that does not need cleanup; we dynamically change the page title based on the form input:

import React, { useState, useEffect } from 'react'

export default function Hello(prop) {
  const name = useFormInput('chanshiyu')

  const title = `Hello, ${name.value}`
  useDocumentTitle(title)

  return (
    <div>
      <Input placeholder="Your name" value={name.value} onChange={name.onChange} />
    </div>
  )
}

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue)
  const handleChange = e => setValue(e.target.value)

  return {
    value,
    onChange: handleChange
  }
}

function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title
  })
}

useEffect lets React know that the component needs to perform some operations after rendering. React will save the function you pass (which we call an “effect”) and call it after performing DOM updates. In the effect of the above example, the passed function sets the document's title property, which will be called after every DOM update.

Placing useEffect inside the component allows us to directly access state variables or other props within the effect. Hooks use JavaScript's closure mechanism to keep them in the function's scope.

By default, useEffect runs after every render. However, you can optimize performance by skipping effects, which will be discussed in detail later.

The function passed to useEffect will differ with each render, and this is intentional. Each re-render generates a new effect, replacing the previous one. In a sense, effects are part of the rendering result — each effect “belongs” to a specific render.

If you are familiar with the lifecycle methods of React classes, you can think of the useEffect Hook as a combination of componentDidMount, componentDidUpdate, and componentWillUnmount. Unlike componentDidMount or componentDidUpdate, effects scheduled with useEffect do not block the browser from updating the screen, making your application feel more responsive. In most cases, effects do not need to be executed synchronously.

Effects that need cleanup#

The side effect of dynamically modifying the tab title is an example of an effect that does not need cleanup, while event listeners are an example of effects that do. To prevent memory leaks, in class components, event listeners are added in componentDidMount and removed in componentWillUnmount, but this can lead to code for the same functionality being spread across two different places, even though both parts of the code affect the same side effect.

In function components, the handling of effects with useEffect is much more elegant; useEffect is designed to execute code that belongs to the same side effect in one place. If your effect returns a function, React will call it when performing cleanup. Here’s another example: we want to set up an event listener for window resize when the component loads and remove it when the component unmounts:

import React, { useState, useEffect } from 'react'

export default function Hello(prop) {
  const width = useWindowWidth()

  return (
    <div>
      <div>Width: {width}</div>
    </div>
  )
}

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth)
  const handleWindowResize = () => setWidth(window.innerWidth)

  useEffect(() => {
    window.addEventListener('resize', handleWindowResize, false)
    // Here we return a function that React will call during cleanup
    return () => window.removeEventListener('resize', handleWindowResize)
  })

  return width
}

Why return a function in the effect? This is an optional cleanup mechanism for effects. Each effect can return a cleanup function. This allows you to keep the logic for adding and removing subscriptions together, as they are part of the same effect.

Concerns with Effects#

One of the purposes of using the Effect Hook is to solve the problem of lifecycle methods in classes often containing unrelated logic while separating related logic into several different methods.

Hooks allow us to separate them by their purpose rather than like lifecycle methods. React will call each effect in the order they were declared in the component. It will clean up the previous effect before calling a new one.

In some cases, executing cleanup or effects after every render can lead to performance issues. In class components, we can solve this by adding logic to compare prevProps or prevState in componentDidUpdate.

componentDidUpdate(prevProps, prevState) {
  if (prevState.name !== this.state.name) {
    document.title = `Hello, ${this.state.name}`
  }
}

In the Effect Hook, the logic for determining whether to re-execute is simpler; it is built into the useEffect Hook API. By passing an array as the second optional parameter to useEffect, React will determine whether the values in the array have changed between renders to decide whether to skip calling the effect, thus optimizing performance. If there are multiple elements in the array, React will execute the effect even if only one element has changed.

useEffect(() => {
  document.title = `Hello, ${this.state.name}`
}, [name])

It is important to note: If you want to use this optimization, ensure that the array contains all variables from the outer scope that will change over time and are used in the effect; otherwise, your code may reference stale variables from the previous render.

If you want to execute an effect that runs only once (only on mount and unmount), you can pass an empty array ([]) as the second parameter. This tells React that your effect does not depend on any values from props or state, so it never needs to re-run.

If you pass an empty array ([]), the props and state inside the effect will always have their initial values.

React will delay calling useEffect until after the browser has painted.

Another point is that when the dependencies of an effect change frequently, you can pass a function instead of a value to setValue inside the effect:

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1) // This effect depends on the `count` state
    }, 1000)
    return () => clearInterval(id)
  }, []) // 🔴 Bug: `count` is not specified as a dependency

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

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1) // ✅ This does not depend on the external `count` variable
    }, 1000)
    return () => clearInterval(id)
  }, []) // ✅ Our effect does not use any variables from the component scope

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

Context Hook#

useContext accepts a context object (the return value of React.createContext) and returns the current value of that context. The parameter for useContext must be the context object itself.

useContext(MyContext) is equivalent to static contextType = MyContext in class components or <MyContext.Consumer>.

The current context value is determined by the nearest <MyContext.Provider>'s value prop above the component. Components that call useContext will re-render whenever the context value changes.

import React, { useContext } from 'react'
import GlobalContext from '../../context'

export default function Hello(prop) {
  const local = useContext(GlobalContext)

  return (
    <div>
      <div>Language: {local}</div>
    </div>
  )
}

Reducer Hook#

In the previous introduction to the State Hook, we extracted the useState for multiple forms into a separate function:

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue)
  const handleChange = e => setValue(e.target.value)

  return {
    value,
    onChange: handleChange
  }
}

This is the precursor to useReducer, which is built into React for managing state. It accepts a reducer of the form (state, action) => newState and returns the current state along with a dispatch method.

When state logic is complex and involves multiple sub-values, or when the next state depends on the previous state, you can use useReducer instead of useState. Additionally, using useReducer can optimize performance for components that trigger deep updates.

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState)

  function dispatch(action) {
    const nextState = reducer(state, action)
    setState(nextState)
  }

  return [state, dispatch]
}

Usage:

function todosReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    // ... other actions ...
    default:
      return state
  }
}

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer, [])

  function handleAddClick(text) {
    dispatch({ type: 'add', text })
  }
  // ...
}

Callback Hook#

useCallback takes an inline callback function and a dependency array as arguments and returns a memoized version of that callback that only changes if one of the dependencies has changed. This is useful when you pass a callback to an optimized child component that relies on reference equality to prevent unnecessary renders (e.g., shouldComponentUpdate).

const memoizedCallback = useCallback(() => {
  doSomething(a, b)
}, [a, b])

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

The dependency array is not passed as an argument to the callback function. Conceptually, it behaves as if all values referenced in the callback should be included in the dependency array.

Using a callback ref can be used to access the DOM:

function MeasureExample() {
  const [height, setHeight] = useState(0)

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height)
    }
  }, []) // [] as the dependency list for useCallback ensures the ref callback does not change on subsequent renders

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  )
}

Or you can extract a reusable Hook:

function MeasureExample() {
  const [rect, ref] = useClientRect()
  return (
    <>
      <h1 ref={ref}>Hello, world</h1>
      {/* Using short-circuit evaluation here */}
      {rect !== null && <h2>The above header is {Math.round(rect.height)}px tall</h2>}
    </>
  )
}

function useClientRect() {
  const [rect, setRect] = useState(null)
  const ref = useCallback(node => {
    if (node !== null) {
      setRect(node.getBoundingClientRect())
    }
  }, [])
  return [rect, ref]
}

Memo Hook#

useMemo returns a memoized value by taking a “create” function and a dependency array as arguments; it only recalculates the memoized value when one of the dependencies changes. This optimization helps avoid expensive calculations on every render. If no dependency array is provided, useMemo will compute a new value on every render.

The function passed to useMemo will execute during rendering. Do not perform side effects or operations unrelated to rendering within this function; such operations belong to useEffect, not useMemo.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

Ref Hook#

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned ref object remains the same throughout the component's lifecycle.

const refContainer = useRef(initialValue)

See the official example:

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>
    </>
  )
}

The only difference between useRef() and creating a {current: ...} object is that useRef returns the same ref object on every render.

The Ref Hook can be used not only for DOM refs. The “ref” object is a generic container that can hold any value with a mutable current property, similar to an instance property of a class.

function Timer() {
  const intervalRef = useRef()

  useEffect(() => {
    const id = setInterval(() => {
      console.log('tick')
    })
    // Record the timer id via the .current property
    intervalRef.current = id

    // Callback cleans up on component unmount
    return () => {
      clearInterval(intervalRef.current)
    }
  })

  // Or you can manually clear it
  function handleCancelClick() {
    clearInterval(intervalRef.current)
  }
}

You can even use it to keep track of the previous props or state:

function Counter() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)
  return (
    <h1>
      Now: {count}, before: {prevCount}
    </h1>
  )
}

function usePrevious(value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

Conceptually, refs can be thought of as instance variables of a class. Unless you are doing lazy loading, avoid setting refs during rendering — this can lead to unexpected behavior. Instead, you should typically modify refs in event handlers and effects.

ImperativeHandle Hook#

useImperativeHandle allows you to customize the instance value that is exposed to parent components when using refs. useImperativeHandle should be used with forwardRef:

function FancyInput(props, ref) {
  const inputRef = useRef()
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    }
  }))
  return <input ref={inputRef} />
}
FancyInput = forwardRef(FancyInput)

LayoutEffect Hook#

useLayoutEffect is similar to useEffect, but it is invoked synchronously after all DOM mutations. You can use it to read the DOM layout and synchronously trigger re-renders. Updates scheduled inside useLayoutEffect will be flushed synchronously before the browser paints. Use standard useEffect whenever possible to avoid blocking visual updates.

DebugValue Hook#

useDebugValue can be used to display a label for custom hooks in React DevTools.

// Display a label next to this Hook in DevTools
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline')

Rules of Hooks#

Hooks are essentially JavaScript functions, but there are two rules you must follow when using them:

  1. Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions — make sure Hooks are called in the same order on every render. This allows React to correctly preserve the state of Hooks across multiple useState and useEffect calls.
  2. Only call Hooks from React functions. Don’t call Hooks from regular JavaScript functions.

React relies on the order of Hook calls to determine which state corresponds to which useState, so it is crucial to ensure that the order of Hooks is consistent on every render. Only if the order of Hook calls is the same on every render can React correctly associate internal state with the corresponding Hook, allowing it to function properly.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.