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 postGET /posts/:id
untuk mengambil detail postGET /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}`)
// ...
}