Data Fetching in React
React doesn't have built-in data fetching. Let's explore the common patterns.
Basic Fetch with useEffect
jsx
1function UserList() {
2 const [users, setUsers] = useState([])
3 const [loading, setLoading] = useState(true)
4 const [error, setError] = useState(null)
5
6 useEffect(() => {
7 const fetchUsers = async () => {
8 try {
9 setLoading(true)
10 const response = await fetch('/api/users')
11
12 if (!response.ok) {
13 throw new Error('Failed to fetch users')
14 }
15
16 const data = await response.json()
17 setUsers(data)
18 } catch (err) {
19 setError(err.message)
20 } finally {
21 setLoading(false)
22 }
23 }
24
25 fetchUsers()
26 }, [])
27
28 if (loading) return <p>Loading...</p>
29 if (error) return <p>Error: {error}</p>
30
31 return (
32 <ul>
33 {users.map(user => (
34 <li key={user.id}>{user.name}</li>
35 ))}
36 </ul>
37 )
38}Custom useFetch Hook
jsx
1function useFetch(url) {
2 const [data, setData] = useState(null)
3 const [loading, setLoading] = useState(true)
4 const [error, setError] = useState(null)
5
6 useEffect(() => {
7 const controller = new AbortController()
8
9 const fetchData = async () => {
10 try {
11 setLoading(true)
12 setError(null)
13
14 const response = await fetch(url, {
15 signal: controller.signal
16 })
17
18 if (!response.ok) {
19 throw new Error(`HTTP error! Status: ${response.status}`)
20 }
21
22 const json = await response.json()
23 setData(json)
24 } catch (err) {
25 if (err.name !== 'AbortError') {
26 setError(err.message)
27 }
28 } finally {
29 setLoading(false)
30 }
31 }
32
33 fetchData()
34
35 return () => controller.abort()
36 }, [url])
37
38 return { data, loading, error }
39}
40
41// Usage
42function UserProfile({ userId }) {
43 const { data: user, loading, error } = useFetch(`/api/users/${userId}`)
44
45 if (loading) return <Spinner />
46 if (error) return <Error message={error} />
47
48 return <h1>{user.name}</h1>
49}Fetching with Dependencies
jsx
1function UserPosts({ userId }) {
2 const [posts, setPosts] = useState([])
3 const [loading, setLoading] = useState(true)
4
5 useEffect(() => {
6 const controller = new AbortController()
7
8 const fetchPosts = async () => {
9 setLoading(true)
10 try {
11 const response = await fetch(`/api/users/${userId}/posts`, {
12 signal: controller.signal
13 })
14 const data = await response.json()
15 setPosts(data)
16 } catch (err) {
17 if (err.name !== 'AbortError') {
18 console.error(err)
19 }
20 } finally {
21 setLoading(false)
22 }
23 }
24
25 if (userId) {
26 fetchPosts()
27 }
28
29 return () => controller.abort()
30 }, [userId]) // Re-fetch when userId changes
31
32 // render...
33}Parallel Fetching
jsx
1function Dashboard({ userId }) {
2 const [data, setData] = useState({
3 user: null,
4 posts: null,
5 comments: null
6 })
7 const [loading, setLoading] = useState(true)
8
9 useEffect(() => {
10 const fetchAll = async () => {
11 setLoading(true)
12
13 try {
14 // Fetch in parallel
15 const [userRes, postsRes, commentsRes] = await Promise.all([
16 fetch(`/api/users/${userId}`),
17 fetch(`/api/users/${userId}/posts`),
18 fetch(`/api/users/${userId}/comments`)
19 ])
20
21 const [user, posts, comments] = await Promise.all([
22 userRes.json(),
23 postsRes.json(),
24 commentsRes.json()
25 ])
26
27 setData({ user, posts, comments })
28 } catch (err) {
29 console.error(err)
30 } finally {
31 setLoading(false)
32 }
33 }
34
35 fetchAll()
36 }, [userId])
37
38 // render...
39}Conditional Fetching
jsx
1function SearchResults({ query }) {
2 const [results, setResults] = useState([])
3 const [loading, setLoading] = useState(false)
4
5 useEffect(() => {
6 // Don't fetch if query is too short
7 if (query.length < 3) {
8 setResults([])
9 return
10 }
11
12 const controller = new AbortController()
13
14 const search = async () => {
15 setLoading(true)
16 try {
17 const response = await fetch(
18 `/api/search?q=${encodeURIComponent(query)}`,
19 { signal: controller.signal }
20 )
21 const data = await response.json()
22 setResults(data)
23 } catch (err) {
24 if (err.name !== 'AbortError') {
25 console.error(err)
26 }
27 } finally {
28 setLoading(false)
29 }
30 }
31
32 search()
33
34 return () => controller.abort()
35 }, [query])
36
37 // render...
38}POST/PUT/DELETE Requests
jsx
1function CreateUser() {
2 const [loading, setLoading] = useState(false)
3 const [error, setError] = useState(null)
4
5 const createUser = async (userData) => {
6 setLoading(true)
7 setError(null)
8
9 try {
10 const response = await fetch('/api/users', {
11 method: 'POST',
12 headers: {
13 'Content-Type': 'application/json'
14 },
15 body: JSON.stringify(userData)
16 })
17
18 if (!response.ok) {
19 throw new Error('Failed to create user')
20 }
21
22 return await response.json()
23 } catch (err) {
24 setError(err.message)
25 throw err
26 } finally {
27 setLoading(false)
28 }
29 }
30
31 const handleSubmit = async (e) => {
32 e.preventDefault()
33 try {
34 const newUser = await createUser({ name, email })
35 console.log('Created:', newUser)
36 } catch {
37 // Error already set in state
38 }
39 }
40
41 // render...
42}API Service Layer
jsx
1// services/api.js
2const API_BASE = '/api'
3
4async function request(endpoint, options = {}) {
5 const url = `${API_BASE}${endpoint}`
6
7 const config = {
8 headers: {
9 'Content-Type': 'application/json',
10 ...options.headers
11 },
12 ...options
13 }
14
15 // Add auth token if available
16 const token = localStorage.getItem('token')
17 if (token) {
18 config.headers.Authorization = `Bearer ${token}`
19 }
20
21 const response = await fetch(url, config)
22
23 if (!response.ok) {
24 const error = await response.json().catch(() => ({}))
25 throw new Error(error.message || 'Request failed')
26 }
27
28 return response.json()
29}
30
31export const api = {
32 get: (endpoint) => request(endpoint),
33
34 post: (endpoint, data) => request(endpoint, {
35 method: 'POST',
36 body: JSON.stringify(data)
37 }),
38
39 put: (endpoint, data) => request(endpoint, {
40 method: 'PUT',
41 body: JSON.stringify(data)
42 }),
43
44 delete: (endpoint) => request(endpoint, {
45 method: 'DELETE'
46 })
47}
48
49// Usage
50import { api } from './services/api'
51
52const users = await api.get('/users')
53const newUser = await api.post('/users', { name: 'John' })
54await api.delete('/users/123')Error Boundary for Data Fetching
jsx
1class ErrorBoundary extends React.Component {
2 state = { hasError: false, error: null }
3
4 static getDerivedStateFromError(error) {
5 return { hasError: true, error }
6 }
7
8 render() {
9 if (this.state.hasError) {
10 return (
11 <div className="error">
12 <h2>Something went wrong</h2>
13 <button onClick={() => window.location.reload()}>
14 Reload page
15 </button>
16 </div>
17 )
18 }
19
20 return this.props.children
21 }
22}
23
24// Usage
25<ErrorBoundary>
26 <UserProfile userId={userId} />
27</ErrorBoundary>These patterns form the foundation of data fetching in React!
