Skip
Arish's avatar

22. Data Fetching Patterns


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!