Tự học ReactJS: React Suspense – So sánh một số cách fetch data từ API

Khi chúng ta fetch data cho một component, chắc chắn mọi người đều muốn lấy data nhanh nhất và sớm nhất có thể. Trước đây, nếu không có React.Suspense chúng ta sẽ gọi API sau khi component mounted. Có lẽ cách này hơi chậm, chúng ta phải chờ cho đến các giai đoạn sau hoàn thành:

  1. Load code
  2. Parse code
  3. Run code
  4. Render component
  5. Component mounted
  6. Gọi API lấy data
  7. Re-render component với data vừa gọi

Trong React, chúng ta có một số cách fetch data (link thao khảo từ React) như sau:

  • Fetch-on-render: render trước, gọi API sau
  • Fetch-then-render: gọi API trước, render sau
  • Fetch-as-render: gọi API và render song song

Fetch-on-render

Ví dụ chúng ta có demo như sau:

Chúng ta sẽ hiển thị thông tin một con pokemon nào đó khi click vào button: pikachu, charizard, mew, … API sẽ được gọi khi component mounted lên DOM (gọi bên trong useEffect). Chúng ta xem log trên network tab thì thứ tự load script và API như sau:

Đầu tiên browser sẽ load script của component PokemonInfo => fallback image => Gọi API lấy data cho pikachu => Pikachu image.

Code demo:

import * as React from 'react'
import {
  PokemonForm,
  PokemonInfoFallback,
  PokemonErrorBoundary,
} from '../../pokemon'

const PokemonInfo = React.lazy(() =>
  import('./lazy/pokemon-info-fetch-on-render'),
)

function App() {
  const [pokemonName, setPokemonName] = React.useState('')

  function handleSubmit(newPokemonName) {
    setPokemonName(newPokemonName)
  }

  function handleReset() {
    setPokemonName('')
  }

  return (
    <div>
      <h1 style={{textAlign: 'center'}}>
        {'Fetch on render '}
        <span role="img" aria-label="thumbs down">
          👎
        </span>
      </h1>
      <PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
      <hr />
      <div className="pokemon-info">
        {pokemonName ? (
          <PokemonErrorBoundary onReset={handleReset} resetKeys={[pokemonName]}>
            <React.Suspense
              fallback={<PokemonInfoFallback name={pokemonName} />}
            >
              <PokemonInfo pokemonName={pokemonName} />
            </React.Suspense>
          </PokemonErrorBoundary>
        ) : (
          'Submit a pokemon'
        )}
      </div>
    </div>
  )
}

export default App
import * as React from 'react'
import {
  fetchPokemon,
  PokemonInfoFallback,
  PokemonDataView,
} from '../../../pokemon'

function PokemonInfo({pokemonName}) {
  const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), {
    pokemon: null,
    error: null,
    status: 'pending',
  })

  const {pokemon, error, status} = state

  React.useEffect(() => {
    let current = true
    setState({status: 'pending'})
    fetchPokemon(pokemonName).then(
      p => {
        if (current) setState({pokemon: p, status: 'success'})
      },
      e => {
        if (current) setState({error: e, status: 'error'})
      },
    )
    return () => (current = false)
  }, [pokemonName])

  if (status === 'pending') {
    return <PokemonInfoFallback name={pokemonName} />
  }

  if (status === 'error') {
    return (
      <div>
        There was an error.
        <pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
      </div>
    )
  }

  if (status === 'success') {
    return (
      <div>
        <div className="pokemon-info__img-wrapper">
          <img src={pokemon.image} alt={pokemon.name} />
        </div>
        <PokemonDataView pokemon={pokemon} />
      </div>
    )
  }
}

export default PokemonInfo

Fetch-then-render

Với cách này, chúng ta sẽ fetch data trước khi render component <PokemonInfo />. Chúng ta sử dụng React.useEffect để goi API bên ngoài component cha, sau khi có đầy đủ data, chúng ta bắt đầu render component con là <PokemonInfo />

import * as React from 'react'
import {fetchPokemon, PokemonForm, PokemonInfoFallback} from '../../pokemon'

const PokemonInfo = React.lazy(() =>
  import('./lazy/pokemon-info-fetch-then-render'),
)

function usePokemon(pokemonName) {
  const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), {
    pokemon: null,
    error: null,
    status: 'pending',
  })

  React.useEffect(() => {
    if (!pokemonName) {
      return
    }
    let current = true
    setState({status: 'pending'})
    fetchPokemon(pokemonName).then(
      p => {
        if (current) setState({pokemon: p, status: 'success'})
      },
      e => {
        if (current) setState({error: e, status: 'error'})
      },
    )
    return () => (current = false)
  }, [pokemonName])

  return state
}

function App() {
  const [pokemonName, setPokemonName] = React.useState('')
  const {pokemon, error, status} = usePokemon(pokemonName)

  function handleSubmit(newPokemonName) {
    setPokemonName(newPokemonName)
  }

  return (
    <div>
      <h1 style={{textAlign: 'center'}}>
        {'Fetch then render '}
        <span role="img" aria-label="thumbs down">
          👎
        </span>
      </h1>
      <PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
      <hr />
      <div className="pokemon-info">
        {pokemonName ? (
          status === 'pending' ? (
            <PokemonInfoFallback name={pokemonName} />
          ) : status === 'error' ? (
            <div>
              There was an error.
              <pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
            </div>
          ) : status === 'success' ? (
            <React.Suspense
              fallback={<PokemonInfoFallback name={pokemonName} />}
            >
              <PokemonInfo pokemon={pokemon} />
            </React.Suspense>
          ) : null
        ) : (
          'Submit a pokemon'
        )}
      </div>
    </div>
  )
}

export default App

Gọi custom hook usePokemon để lấy pokemon data và chỉ show <PokemonInfo pokemon={pokemon} /> một khi status === 'success'

Chúng ta thấy với fetch-then-render chúng ta sẽ gọi API trước, sau đó load script cho component <PokemonInfo />

Fetch-as-render

Fetch-as-render có nghĩa là vừa fetch vừa render component, chúng ta sẽ thực hiện hai việc này song song với nhau.

Bạn có thể thấy gần như API (https://graphql-pokemon2.vercel.app/) và chunk.js (lazy-loading script cho component PokemonInfo) được chạy cùng một lúc.

Bạn có thể tham khảo demo sau đây

import * as React from 'react'
import {fetchPokemon, PokemonDataView} from '../pokemon'

// gọi api để lấy data ngay khi load file này lên
let pokemon
let pokemonPromise = fetchPokemon('pikachu').then(p => (pokemon = p))

function PokemonInfo() {
// React Suspense sẽ throw một promise và tư động
// re-render component PokemonInfo ngay khi promise ở trên được resolve

  if (!pokemon) {
    throw pokemonPromise
  }
  return (
    <div>
      <div className="pokemon-info__img-wrapper">
        <img src={pokemon.image} alt={pokemon.name} />
      </div>
      <PokemonDataView pokemon={pokemon} />
    </div>
  )
}

function App() {
  return (
    <div className="pokemon-info-app">
      <div className="pokemon-info">
        <React.Suspense fallback={<div>Loading Pokemon...</div>}>
          <PokemonInfo />
        </React.Suspense>
      </div>
    </div>
  )
}

export default App

Bạn có thể tham khảo chi tiết về React.Suspense ở bài viết trước.

Tóm lại với React.Suspenseconcurrent mode, chúng ta sẽ tăng tốc website bằng cách sử dụng kỹ thuật tải dữ liệu và render song song. Và tận dụng thời gian chờ render để gọi API. Do đó thời gian sẽ ngắn hơn, người dùng sẽ được tương tác với trang web sớm hơn.

Nguồn: Epic React by Kent C.Dodds

3 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *