#guide #indonesian

Better Data Fetching in React

December 2023 — by @rulasfia

Bayangkan kita membuat sebuah aplikasi web semacam twitter untuk menampilkan post dari user. Aplikasi ini akan terdiri dari dua halaman, yaitu halaman beranda yang menampilkan semua post dan halaman post detail yang menampilkan post yang dimaksud beserta komentar terkait.

Untuk sementara, kita akan menggunakan 3 API endpoint berikut.

  • GET /posts untuk mengambil semua post
  • GET /posts/:id untuk mengambil detail post
  • GET /posts/:id/comments untuk mengambil komentar dari sebuah post

The React way

Pada saat artikel ini ditulis, React Core Team yang bertanggung jawab dalam pengembangan React telah mengeluarkan RFC atau proposal terkait hooks baru yang dapat digunakan untuk menghandle Promise secara langsung. Salah satu kegunaan dari hooks ini nantinya adalah untuk melakukan fetching data ke API. Namun ini masih berstatus proposal dan masih jauh dari kata siap.

React memang sengaja tidak menyediakan metode khusus untuk melakukan fetching data ke API secara langsung. Ini sesuai dengan filosofi React yang hanya fokus menjadi UI library dan membebaskan kita sebagai developer untuk menggunakan library yang sesuai dengan preferensi untuk styling, routing, state management, dll.

Sehingga yang dapat kita lakukan untuk saat ini adalah melakukan fetching data di awal fase render (componentDidMount) dan menyimpan hasil request tersebut pada state yang telah disiapkan. Pada implementasinya, kita dapat menggunakan useEffect yang dipanggil dengan array kosong sebagai dependency.

function Home() {
  const [posts, setPosts] = useState<Post[]>([])

  useEffect(() => {
    fetch(`${API_URL}/posts`)
      .then(res => res.json())
      .then(data => setPosts(data as Post[]))
  }, [])

  // ...
}

Secara sekilas, terlihat tidak ada masalah pada implementasi tersebut. Bahkan mungkin itu sudah cukup untuk aplikasi sederhana dengan skala kecil.

Namun seiring aplikasi berkembang, requirement aplikasi juga menjadi semakin rumit. Sehingga kita perlu menghandle request data ketika berada pada fase loading dan ketika terjadi error.

type RequestStatusType = 'pending' | 'loading' | 'error' | 'success'

function Home() {
  const [posts, setPosts] = useState<Post[]>([])
  const [status, setStatus] = useState<RequestStatusType>('pending')
  const [error, setError] = useState('')

  useEffect(() => {
    setStatus('loading')
    fetch(`${API_URL}/posts`)
      .then(res => res.json())
      .then(data => {
        // using type assertion because fetch() doesn't support ts
        setPosts(data as Post[])
        setStatus('success')
      })
      .catch(err => {
        // transform error to user readable format
        const msg = formatError(err)
        setError(msg)
        setStatus('error')
      })
  }, [])

  // ...
}

Dengan implementasi tersebut, kita dapat menggunakan informasi request status untuk menampilkan UI yang relevant kepada user, baik ketika fase loading, error, atau ketika request telah success dan data dapat ditampilkan.

Masalah baru muncul ketika kita ingin melakukan data fetching di component lain. Kita bisa saja melakukan copy-paste dan menggunakan implementasi tersebut pada component lain. Namun cara yang lebih baik yaitu meng-extract implementasi tersebut pada sebuah function terpisah yang dapat kita gunakan kapanpun pada component yang perlu melakukan data fetching.

// utils.ts
type RequestStatusType = 'pending' | 'loading' | 'error' | 'success'

export function useFetch<TResult>(url: string) {
  // using generic type for the result
  const [data, setData] = useState<TResult | null>(null)
  const [status, setStatus] = useState<RequestStatusType>('pending')
  const [error, setError] = useState('')

  useEffect(() => {
    setStatus('loading')
    fetch(url)
      .then(res => res.json())
      .then(data => {
        // using type assertion because fetch() doesn't support ts
        setData(data as ResultType)
        setStatus('success')
      })
      .catch(err => {
        // transform error to user readable format
        const msg = formatError(err)
        setError(msg)
        setStatus('error')
      })
  }, [])

  return { data, status, error }
}
// home.tsx
import { useFetch } from './utils'

function Home() {
  const { data, status, error } = useFetch<Post[]>(`${API_URL}/posts`)

  // ...
}

// post-detail.tsx
import { useFetch } from './utils'

function PostDetail() {
  const { id } = useParams()
  const { data, status, error } = useFetch<Post>(`${API_URL}/posts/${id}`)

  // ...
}

There is a better way