六個 React 常見可優化的用法

在這篇文章中,我探討了六個常見的 React 優化技巧,幫助開發者避免常犯錯誤並提升應用性能。例如,當不需要即時取得輸入值時,使用 useRef 代替 useState 可以避免不必要的渲染;理解 setState 的異步特性和使用上一狀態的重要性;以及正確使用 useMemo 來避免因引用類型更新導致的額外渲染。這些技巧不僅提高了代碼的效率,也加深了對 React 內部工作機制的理解。

六個 React 常見可優化的用法
Photo by Fili Santillán / Unsplash

1. 不需要及時取得值的時候用 useState

在此狀況中不需要及時的取得輸入值,只需要按下 submit 按鈕時取得即可。

  • 待優化的用法
import { useState } from "react"

function App(){
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")

  function onSubmit(e) {
    e.preventDefault()
    console.log({email, password})
  }


  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="email">Email</label>
      <input 
        value={email}
        onChange={e => setEmail(e.target.value)} 
        type="email" 
        id=email
      >
      <label htmlFor="password">Password</label>
      <input 
        value={password}
        onChange={e => setPassword(e.target.value)} 
        type="password" 
        id="password"
      >
      <button type="submit">Submit</button>
    </form>
  )
}

export default App
  • 優化後的用法
import { useRef } from "react"

function App(){
  const emailRef = useRef()
  const passwordRef = useRef()

  function onSubmit(e) {
    e.preventDefault()
    console.log({
      email: emailRef.current.value,
      password: passwordRef.current.value 
    })
  }


  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="email">Email</label>
      <input ref={emailRef} type="email" id=email>
      <label htmlFor="password">Password</label>
      <input ref={passwordRef} type="password" id="password">
      <button type="submit">Submit</button>
    </form>
  )
}

export default App

2. 沒有正確了解 setState 之 previous value 的特性

  • 待優化的用法
import { useState } from "react"

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

  const adjustCount = amount => {
  // 0 + 1
  setCount(count + amount)
  // 0 + 1
  setCount(count + amount)
  }

  return (
    <>
      <button onClick={() => adjustCount(-1)}>-</button>
      <span>{count}</span>
      <button onClick={() => adjustCount(+1)}>-</button>
    </>
  )
}
  • 優化後的用法
import { useState } from "react"

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

  const adjustCount = amount => {
    // 0 + 1
    setCount(prev => {
      return prev + amount
    })

    // 1 + 2
    setCount(prev => {
      return prev + amount
    })
  }

  return (
    <>
      <button onClick={() => adjustCount(-1)}>-</button>
      <span>{count}</span>
      <button onClick={() => adjustCount(+1)}>-</button>
    </>
  )
}

3. 沒有正確了解 setState 之非同步的特性

  • 待優化的用法
import { useState } from "react"

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

  const adjustCount = amount => {
    setCount(prev => {
      return prev + amount
    })
    console.log(count) // 0
  }

  return (
    <>
      <button onClick={() => adjustCount(-1)}>-</button>
      <span>{count}</span>
      <button onClick={() => adjustCount(+1)}>-</button>
    </>
  )
}
  • 優化後的用法
import { useState, useEffect } from "react"

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

  const adjustCount = amount => {
    setCount(prev => {
      return prev + amount
    })
  }

  useEffect(() => {
    console.log(count)
  }, [count])

  return (
    <>
      <button onClick={() => adjustCount(-1)}>-</button>
      <span>{count}</span>
      <button onClick={() => adjustCount(+1)}>-</button>
    </>
  )
}

4. 沒有正確了解物件 call by reference 的不同

當改變 darkMode,Component 會重複渲染,即使 age 和 name 值是一樣,但物件的位置改變了也會觸發 useEffect。

useMemo 則是會在 dependencies 沒有改變的情況下,把某個運算的結果保存下來,在此例子中會在物件中的值沒有改變的狀況下,把原始物件保存下來。

  • 待優化的用法
import { useState, useEffect } from "react"

export function Counter(){
  const [age, setAge] = useState(0)
  const [name, setName] = useState("")
  const [darkMode, setDarkMode] = useState(false)

  const person = { age, name}

  useEffect(() => {
    console.log(person)
  }, [person])



  return (
    <div>
      Age: {" "}
      <input value={age} type="number" onChange={e => setAge(e.target.value)} />
      <br />
      Name: {" "} 
      <input value={name}  onChange={e => setAge(e.target.value)} />
      <br />
      Dark Mode: {" "}
      <input
        type="checkbox"
        value={darkMode}
        onChange={e => setDarkMode(e.target.value)}
      />
    </div>
  )
}
  • 優化後的用法
import { useState, useEffect, useMemo } from "react"

export function Counter(){
  const [age, setAge] = useState(0)
  const [name, setName] = useState("")
  const [darkMode, setDarkMode] = useState(false)

  const person = useMemo(() => {
    return {age, name}
  }, [agem name])

  useEffect(() => {
    console.log(person)
  }, [person])

  return (
    <div>
      Age: {" "}
      <input value={age} type="number" onChange={e => setAge(e.target.value)} />
      <br />
      Name: {" "} 
      <input value={name}  onChange={e => setAge(e.target.value)} />
      <br />
      Dark Mode: {" "}
      <input
        type="checkbox"
        value={darkMode}
        onChange={e => setDarkMode(e.target.value)}
      />
    </div>
  )
}

5. 不必要的重複使用 useState 導致需要不必要的 useEffect

  • 待優化的用法
import { Counter } from "react"

function App(){
  const [firstName, setFirstName] = useState("")
  const [firstName, setLastName] = useState("")
  const [fullName, setFullName] = useState("")

  useEffect(() => {
    setFullName(`${firstName} ${LastName}`)
  }, [firstName, lastName])

  return (
    <input value={firstName} onChange={e => setFirstName(e.target.value)}>
    <input value={lastName} onChange={e => setLastName(e.target.value)}>
    {fullName}
  )
}

export default App
  • 優化後的用法
import { Counter } from "react"

function App(){
  const [firstName, setFirstName] = useState("")
  const [firstName, setLastName] = useState("")
  const fullName = `${firstName} ${LastName}`


  return (
    <input value={firstName} onChange={e => setFirstName(e.target.value)}>
    <input value={lastName} onChange={e => setLastName(e.target.value)}>
    {fullName}
  )
}

export default App

6. 在 useEffect 中沒有考慮到非同步的問題使用 fetch

  • 待優化的用法
import { useEffect } from "react"

export function useFetch()
  const [loading, setLoading] = useState(true)
  const [data, setData] = useState()
  const [error, setError] = useState()

  useEffect(() => {
    setLoading(true)
    fetch(url)
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false))
  }, [url])

  return {loading, data, error}
}

// 0. 0ms   + 50ms  = 50ms
// 1. 100ms + 300ms = 400ms
// 3. 200ms + 100ms = 300ms
  • 優化後的用法
import { useEffect } from "react"

export function useFetch()
  const [loading, setLoading] = useState(true)
  const [data, setData] = useState()
  const [error, setError] = useState()

  useEffect(() => {
    const controller = new AbortController()
    setLoading(true)
    fetch(url, {signal: controller.signal})
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false))
    
    return () => {
      controller.absort()
    }
  }, [url])
}

資料來源:Top 6 React Hook Mistakes Beginners Make