Tự học ReactJS: React Suspense – Lấy dữ liệu từ API

Concurrent mode là gì?

Đây là một kỹ thuật cho phép chúng ta thực hiện một số công việc (như fetch data từ api) song song với quá trình render component.

Nó giúp tăng tốc trang web khi load dữ liệu từ server, đặc biệt với các thiết bị low-end như mobile. Lưu ý rằng API của concurrent mode vẫn còn đang trong quá trình thử nghiệm.

Tại sao cần React.Suspense và concurrent mode?

Trong React, một khi bạn muốn fetch data thì bạn sẽ làm như thế nào? Có phải là bạn sẽ gọi API trong React.useEffect, có nghĩa là bạn gọi API sau khi component đã render. Ví dụ như thế này:

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

// render component

Vậy liệu có cách nào bạn vừa render app và vừa fetch data. Với concurrent mode bạn sẽ làm được điều đó. Điều này sẽ giúp bạn tận dụng tối đa thời gian để fetch data trước khi component render. Bạn sẽ gọi API trước và chuẩn bị sẵn sàng data để render component. Nó giúp giảm thời gian render component. Thậm chí bạn có thể fetch data trước khi toàn app render.

Ý tưởng cơ bản là:

function Component() {
  if (data) {
    return <div>{data.message}</div>
  }
  throw promise
  // React sẽ theo dõi throw promise và tìm "Suspense" component gần nhất
  // và chờ quá trình render cho đến khi promise được hoàn thành
}

ReactDOM.createRoot(rootEl).render(
  <React.Suspense fallback={<div>loading...</div>}>
    <Component />
  </React.Suspense>,
)

React.Suspense sẽ chờ một cái gì đó (gọi API) hoàn thành trước khi component có thể render và trong thời gian chờ nó sẽ hiển thị fallback. Khi wrap component bên trong React.Suspense thì component sẽ chờ cho đến khi promise được hoàn thành, sau đó component mới được render.

Và tất nhiên là React chỉ chờ một mình <Component />, những component khác trong app được render bình thường.

Promise

Ôn lại một tí về Promise, sau đây là ví dụ đơn giản về cách viết và sử dụng Promise:

const handleSuccess = result => console.log(result)
const handleFailure = error => console.error(error)

const myPromise = someAsyncFunction().then(handleSuccess, handleFailure)

Fetch data cho pokemon

Chúng ta sẽ build một component nhỏ để lấy dữ liệu cho Pikachu bằng kỹ thuật concurrent mode và suspense. Chúng ta sẽ goi API trước khi component render, kết quả sẽ như sau:

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

// 💣 xoá dòng này...
const pokemon = {
  name: 'TODO',
  number: 'TODO',
  attacks: {
    special: [{name: 'TODO', type: 'TODO', damage: 'TODO'}],
  },
  fetchedAt: 'TODO',
}

function PokemonInfo() {
  // 🐨 nếu chưa có pokemon throw the pokemonPromise
  // 💰 (no, for real. Like: `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">
        {/* 🐨 Bọc PokemonInfo component với a React.Suspense component và fallback */}
        <PokemonInfo />
      </div>
    </div>
  )
}

export default App

Đáp án:

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

// pokemonPromise được khai báo bên ngoài component, cho nên khi file này
// được chạy qua chúng ta đã gọi API trước khi component render
let pokemon
let pokemonPromise = fetchPokemon('pikachu').then(p => (pokemon = p))

function PokemonInfo() {
  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

Handle error trong component

Giả sử bạn truyền sai tên “pikachu” thành “pikaka”, component PokemonInfo sẽ không render gì và cũng ko có thông tin gì về lỗi đó.

Trong React, bạn có thể handle error trong component với ErrorBoundary để component hiển thị rõ hơn về lỗi đó. Điều này giúp bạn có thể nhận biết được vấn đề và fix nó nhanh hơn.

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

let pokemon
let pokemonError
let pokemonPromise = fetchPokemon('pikaâchu').then(
  p => (pokemon = p),
  e => (pokemonError = e),
)

function PokemonInfo() {
  if (pokemonError) {
    throw pokemonError
  }
  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">
        <PokemonErrorBoundary>
          <React.Suspense fallback={<div>Loading Pokemon...</div>}>
            <PokemonInfo />
          </React.Suspense>
        </PokemonErrorBoundary>
      </div>
    </div>
  )
}

export default App

Nếu có ErrorBoundary thì khi promise bị lỗi, component sẽ hiển thị:

Tạo hàm createResource để sử dụng khi fetch data

Chúng ta sẽ tạo một hàm dùng chung createResource để gọi lấy data từ API khi cần thiết. Chúng ta chỉ cần truyền promise vào hàm đó. Những xử lí còn lại sẽ do hàm createResource phụ trách.

Ví dụ chúng tạo hàm createResource như sau:

const resource = createResource(someAsyncThing())

function MyComponent() {
  const myData = resource.read()
  // render myData stuff
}

Đáp án:

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

let pokemonResource = createResource(fetchPokemon('pikachu'))

function createResource(promise) {
  let status = 'pending'
  let result = promise.then(
    resolved => {
      status = 'success'
      result = resolved
    },
    rejected => {
      status = 'error'
      result = rejected
    },
  )
  return {
    read() {
      if (status === 'pending') throw result
      if (status === 'error') throw result
      if (status === 'success') return result
      throw new Error('This should be impossible')
    },
  }
}

function PokemonInfo() {
  const pokemon = pokemonResource.read()
  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">
        <PokemonErrorBoundary>
          <React.Suspense fallback={<div>Loading Pokemon...</div>}>
            <PokemonInfo />
          </React.Suspense>
        </PokemonErrorBoundary>
      </div>
    </div>
  )
}

export default App

Nguồn: Epic React by Kent C.Dodds

4 Comments

Leave a Reply

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