React 19 Beta 新武器解鎖體驗

#react #前端框架
React 19 Beta 新武器解鎖體驗
五倍技術部
技術文章
React 19 Beta 新武器解鎖體驗

經過長時間的等待,自從 React 18 在 2022 年推出以來,React 19 Beta 版終於在近期與開發者見面了。作為 React 18 的下一個主要版本,React 19 帶來了許多讓開發者心動的新特性,這次我們就先來一探究竟有什麼新的功能。

useTransition

在前端操作中,經常會碰到需要更新狀態的情況,例如:使用者點擊按鈕後,需要更新 UI 來響應這個操作。然而,有些狀態更新可能計算量較大或者涉及非同步操作,執行時間較長。

以下是一個範例,當使用者輸入名字後,點擊按鈕,會呼叫 API 更新名字,並且在更新過程中,按鈕會變成不可點擊的狀態。updateNameAPI 這個函式會模擬 API 的行為。

import { useState } from 'react'

const updateNameAPI = async (newName) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const shouldError = newName.includes('error')
      if (shouldError) {
        reject(`名字不能包含"error"`)
      } else {
        resolve(`你的新名字是: ${newName}`)
      }
    }, 1000)
  })
}

const Pending = () => {
  const [name, setName] = useState('')
  const [error, setError] = useState(null)
  const [isPending, setIsPending] = useState(false)

  const handleSubmit = async () => {
    setIsPending(true)
    const error = await updateNameAPI(name)
    setIsPending(false)
    if (error) {
      setError(error)
      return
    }
  }

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        {isPending ? '更新中...' : '更新名字'}
      </button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  )
}

可以看到,這段程式碼中,我們使用了三個 useState 來管理狀態,並且在 handleSubmit 中,使用 setIsPending 來控制按鈕是否可點擊,以及在 API 回傳錯誤時,使用 setError 來顯示錯誤訊息。

useTransition 這個 hook 已經不是新東西了,它在 React 18 中就已經出現,但在 React 19 中,它被進一步改進,讓我們更容易地處理非同步操作。

import { useState, useTransition } from 'react'

const Pending = () => {
  const [name, setName] = useState('')
  const [error, setError] = useState(null)
  const [isPending, startTransition] = useTransition()

  const handleSubmit = () => {
    startTransition(async () => { // 使用 startTransition 包裹非同步操作
      const error = await updateNameAPI(name)
      if (error) {
        setError(error)
        return
      }
    })
  }

  // startTransition 會自動處理 pending 狀態
  // 在非同步操作期間,isPending 為 true 
  // 操作完成後,isPending 變為 false

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        {isPending ? '更新中...' : '更新名字'}
      </button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  )
}

這裡我們簡單地使用 useTransition 來取代 setIsPending,在 useTransition 每次非同步操作時,會自動幫我們處理好 pending 的狀態,讓我們不需要再手動管理。

useActionState

前面展示了 useTransition 已經簡化了一些程式碼,接下來看到 useActionState 這個新 hook,可以更進一步簡化我們的程式碼:

import { useActionState } from 'react'

const UseActionStateExample = () => {
  const [error, submitAction, isPending] = useActionState(
    async (state, formData) => {
      const newName = formData.get('name')
      const error = await updateNameAPI(newName)
      if (error) {
        return error
      }
    }
  )

  const handleSubmit = (e) => {
    e.preventDefault()
    const formData = new FormData(e.target)
    submitAction(formData) // 調用 submitAction 提交表單
  }

  // useActionState 自動管理 error 和 isPending 狀態
  // 在非同步操作期間,isPending 為 true
  // 操作完成後,根據返回值更新 error 狀態

  return (
    <form onSubmit={handleSubmit}>
      <input name='name' />
      <button type='submit' disabled={isPending}>
        {isPending ? '更新中...' : '更新名字'}
      </button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </form>
  )
}

useActionState 分別回傳了三個值,第一個是錯誤訊息,第二個是一個函式,用來提交表單,第三個是一個布林值,用來判斷是否正在執行中。

這個例子中,我們完全不需要 useState,只需要使用 useActionState 就可以完成前面使用 useTransition 所有的操作,這樣可以讓我們的程式碼更加簡潔。不過眼尖的你,可能也發現到這個例子中,使用的是 form 表單的操作,所以這也表示 useActionState 只能用在表單操作上。

useFormStatus

在以前的 React 中,如果按鈕元件獨立出來,要讓按鈕元件知道目前表單是否正在執行中,需要透過 props 傳遞,這樣會讓程式碼變得複雜,不過在 React 19 中,我們可以使用 useFormStatus 來取得表單的狀態:

import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'

const SubmitButton = () => {
  const { pending } = useFormStatus()
  // 從 useFormStatus 獲取 pending 狀態
  return (
    <button type='submit' disabled={pending}>
      {pending ? '更新中...' : '更新名字'}
    </button>
  )
}

// useFormStatus 讀取父 <form> 的 pending 狀態
// 無需通過 props 傳遞,降低了組件耦合

const UseFormStatus = () => {
  const [error, submitAction] = useActionState(async (state, formData) => {
    const newName = formData.get('name')
    const error = await updateNameAPI(newName)
    if (error) {
      return error
    }
  })

  const handleSubmit = async (formData) => {
    submitAction(formData)
  }

  return (
    <form action={handleSubmit}>
      <input name='name' />
      <SubmitButton />
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </form>
  )
}

在上面的例子中,示範了使用 useFormStatus 搭配 useActionState 來簡化表單的操作,前者可以不需要傳遞 props 就可以直接幫我們處理運作中的狀態,這樣後者也可以不需要取出 isPending 來判斷是否正在執行中。

useOptimistic

在某些情境下,當使用者提交表單時,我們可以先假設使用者的操作是成功的,然後再等待 API 回傳結果,這樣可以讓使用者感受到更快的回饋,這個概念就是所謂的 Optimistic UI,在 React 19 中,我們可以使用 useOptimistic 來實現這個功能:

import { useState } from 'react'
import { useOptimistic } from 'react'

const OptimisticExample = () => {
  const [title, setTitle] = useState('React 18')
  const [optimisticTitle, setOptimisticTitle] = useOptimistic(title)

  const submitForm = async () => {
    const newTitle = 'React 19'
    setOptimisticTitle(newTitle) // 立即渲染 optimistic 狀態
    const res = await new Promise((resolve) => {
      setTimeout(() => {
        resolve(`${newTitle} Beta`)
      }, 1000)
    })
    setTitle(res) // 更新為最終結果
  }

  // useOptimistic 使得我們可以優雅地實現 optimistic UI
  // 先展示期望的最終狀態,等待非同步操作完成後再更新為實際結果

  return (
    <form action={submitForm}>
      <h1>{optimisticTitle}</h1>
      <button type='submit'>更新</button>
    </form>
  )
}

在這個例子中,我們使用 useOptimistic 來取代 useState,這樣就可以讓我們在提交表單時,先更新 UI,然後再等待 API 回傳結果,這樣可以讓使用者感受到更快的回饋。

use

在之前版本的 React 中,如果想讀取一些外部的 API 資料,一般都會使用 useEffect 來處理,可以先看以下的範例:

import { useState, useEffect } from 'react'

const Messages = ({ messages }) => {
  return (
    <ul>
      {messages.map((message) => (
        <li key={message.id}>
          <h2>{message.title}</h2>
          <p>{message.body}</p>
        </li>
      ))}
    </ul>
  )
}

function FetchExample() {
  const [messages, setMessages] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const fetchData = async () => {
      try {
        const res = await fetch('https://jsonplaceholder.typicode.com/posts')
        const data = await res.json()
        setMessages(data)
        setLoading(false)
      } catch (err) {
        console.error(err)
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [])

  if (loading) {
    return <p>⌛Downloading message...</p>
  }

  return <Messages messages={messages} />
}

在 React 19 中,我們可以使用 use 來取代 useEffect,這樣可以讓我們的程式碼更加簡潔:

import { use, Suspense } from 'react'

const fetchData = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts')
  return res.json()
}

const Messages = () => {
  const messages = use(fetchData())
  // use 讀取非同步資源,並暫停渲染直到完成

  return (
    <ul>
      {messages.map((message) => (
        <li key={message.id}>
          <h2>{message.title}</h2>
          <p>{message.body}</p>
        </li>
      ))}
    </ul>
  )
}

// use 簡化了非同步讀取資源的實現
// 不需要使用 useEffect 提高可讀性

function UseExample() {
  return (
    <Suspense fallback={<p>⌛Downloading message...</p>}>
      <Messages />
    </Suspense>
  )
}

Context

最後來介紹 React 19 的 Context,這個部分算是有點小變動,以往在使用 Context 的時候,需要加上一個 Provider,像是 <Context.Provider>

但在 React 19 中,我們可以直接使用 Context 來取代 Provider,這樣可以讓我們的程式碼更加簡潔:

import { createContext, useContext } from 'react'

// 新建 Context
const ThemeContext = createContext('light')

function ThemeProvider({ children }) {
  const theme = 'light'

  // 使用新語法渲染 <Context> 作為 provider
  return <ThemeContext value={theme}>{children}</ThemeContext>
}

function ThemedButton() {
  // 使用 Context
  const theme = useContext(ThemeContext)

  return (
    <button
      style={{
        backgroundColor: theme === 'light' ? 'white' : 'black',
        color: theme === 'light' ? 'black' : 'white'
      }}
    >
      I am a {theme} themed button
    </button>
  )
}

const ContextExample = () => {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  )
}

在這個例子中,當傳遞 valuelight 時,ThemedButton 會顯示一個白色的按鈕,當傳遞 valuedark 時,ThemedButton 會顯示一個黑色的按鈕。

結論

React 19 Beta 還有其他功能,這篇文章只是挑出一些常用的情境可使用的 hook 和 API 來介紹,想了解更多的話,在 React 19 正式版推出再來做更加詳細的介紹。

總的來說,React 19 Beta 為開發者帶來了許多改變,讓非同步渲染、數據獲取和更新的體驗獲得極大提升。雖然這只是一個 Beta 版本,但這些新功能和改進都值得我們熱烈期待 React 19 的正式到來。最後讓我們拭目以待,並為即將到來的 React 開發新紀元做好準備吧!

本文引用自 Bucky's Code Journey 中的 React 19 Beta 新武器解鎖體驗