文件
Course 3

Course 3

導讀概念 / 導讀目的

  • 瞭解 React 中常用的 Hook 觀念

導讀內容

Hooks - Introducing Hooks (opens in a new tab)

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.

[Video] React Today and Tomorrow and 90% Cleaner React With Hooks (opens in a new tab)

Motivation

  • It’s hard to reuse stateful logic between components
  • Complex components become hard to understand
  • Classes confuse both people and machines

Hooks - Hooks at a Glance (opens in a new tab)

✌️ Rules of Hooks

Hooks are JavaScript functions, but they impose two additional rules:

  • Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions.
  • Only call Hooks from React function components. Don’t call Hooks from regular JavaScript functions. (There is just one other valid place to call Hooks — your own custom Hooks. We’ll learn about them in a moment.)

We provide a linter plugin (opens in a new tab) to enforce these rules automatically. We understand these rules might seem limiting or confusing at first, but they are essential to making Hooks work well.

:::tip You can learn more about these rules on a dedicated page: Rules of Hooks (opens in a new tab). :::

Hooks - useState (opens in a new tab)

useState returns an array with two values, the first being the state and the second being the function used to update the state. Whenever the state value changes, it will trigger a re-render.

import React, { useState } from 'react'
 
function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0)
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}
const countStateVariable = useState(0) // Returns a pair
const count = countStateVariable[0] // First item in a pair
const setCount = countStateVariable[1] // Second item in a pair

useState

const [state, setState] = useState(initialState)

Returns a stateful value, and a function to update it.

During the initial render, the returned state (state) is the same as the value passed as the first argument (initialState).

The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component.

setState(newState)

During subsequent re-renders, the first value returned by useState will always be the most recent state after applying updates.

:::tip React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list. :::

Functional updates

If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value. Here’s an example of a counter component that uses both forms of setState:

function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount)
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount((prevCount) => prevCount - 1)}>-</button>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
    </>
  )
}

:::tip Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:

const [state, setState] = useState({})
setState((prevState) => {
  // Object.assign would also work
  return { ...prevState, ...updatedValues }
})

Another option is useReducer, which is more suited for managing state objects that contain multiple sub-values. :::

Hooks - useEffect (opens in a new tab)

The Effect Hook lets you perform side effects in function components:

import React, { useState, useEffect } from 'react'
 
function Example() {
  const [count, setCount] = useState(0)
 
  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`
  })
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

image

import React, { useState, useEffect } from 'react'
 
function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null)
 
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline)
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
    }
  })
 
  if (isOnline === null) {
    return 'Loading...'
  }
  return isOnline ? 'Online' : 'Offline'
}

useEffect

useEffect(didUpdate)

Accepts a function that contains imperative, possibly effectful code.

Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as React’s render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.

Instead, use useEffect. The function passed to useEffect will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.

By default, effects run after every completed render, but you can choose to fire them only when certain values have changed (opens in a new tab).

Hooks - useContext (opens in a new tab)

const value = useContext(MyContext)

Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest <MyContext.Provider> above the calling component in the tree.

When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo (opens in a new tab) or shouldComponentUpdate (opens in a new tab), a rerender will still happen starting at the component itself using useContext.

Don’t forget that the argument to useContext must be the context object itself:

  • Correct: useContext(MyContext)
  • Incorrect: useContext(MyContext.Consumer)
  • Incorrect: useContext(MyContext.Provider)

A component calling useContext will always re-render when the context value changes. If re-rendering the component is expensive, you can optimize it by using memoization (opens in a new tab).

Putting it together with Context.Provider

const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
}
 
const ThemeContext = React.createContext(themes.light)
 
function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  )
}
 
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  )
}
 
function ThemedButton() {
  const theme = useContext(ThemeContext)
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  )
}

image

Hooks - useReducer (opens in a new tab)

const [state, dispatch] = useReducer(reducer, initialArg, init)

An alternative to useState (opens in a new tab). Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks (opens in a new tab).

Here’s the counter example from the useState (opens in a new tab) section, rewritten to use a reducer:

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 Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  )
}

Specifying the initial state

There are two different ways to initialize useReducer state. You may choose either one depending on the use case. The simplest way is to pass the initial state as a second argument:

const [state, dispatch] = useReducer(reducer, { count: initialCount })

Hooks - useCallback (opens in a new tab)

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

Returns a memoized (opens in a new tab) callback.

Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

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

Hooks - useMemo (opens in a new tab)

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

Returns a memoized (opens in a new tab) value.

Pass a "create" function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.

Remember that the function passed to useMemo runs during rendering. Don’t do anything there that you wouldn’t normally do while rendering. For example, side effects belong in useEffect, not useMemo.

If no array is provided, a new value will be computed on every render.

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to "forget" some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

Hooks - useRef (opens in a new tab)

const refContainer = useRef(initialValue)

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.

A common use case is to access a child imperatively:

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

Essentially, useRef is like a "box" that can hold a mutable value in its .current property.

You might be familiar with refs primarily as a way to access the DOM (opens in a new tab). If you pass a ref object to React with <div ref={myRef} />, React will set its .current property to the corresponding DOM node whenever that node changes.

However, useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around (opens in a new tab) similar to how you’d use instance fields in classes.

This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.

Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref (opens in a new tab) instead.

Hooks - Custom Hook (opens in a new tab)

Building your own Hooks lets you extract component logic into reusable functions.

Traditionally in React, we’ve had two popular ways to share stateful logic between components: render props (opens in a new tab) and higher-order components (opens in a new tab). We will now look at how Hooks solve many of the same problems without forcing you to add more components to the tree.

Extracting a Custom Hook

When we want to share logic between two JavaScript functions, we extract it to a third function. Both components and Hooks are functions, so this works for them too!

A custom Hook is a JavaScript function whose name starts with "use" and that may call other Hooks.

Do I have to name my custom Hooks starting with "use"? Please do. This convention is very important. Without it, we wouldn’t be able to automatically check for violations of rules of Hooks (opens in a new tab) because we couldn’t tell if a certain function contains calls to Hooks inside of it.

Do two components using the same Hook share state? No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.

參考