I think we can finally stop writing `useEffect` for data fetching.

I think we can finally stop writing `useEffect` for data fetching.

Leader posted 2 min read

Honest talk: If you’re like me, you probably have a few thousand lines of code that look exactly the same: useState for data, useState for loading, useState for error, and a messy useEffect to tie it all together.

React 19’s new use() API completely retires this pattern, allowing you to read the result of a Promise right inside your component's render function. It genuinely simplifies asynchronous data fetching.

Let's look at the new flow, focusing on the two biggest questions developers have: loading states and error handling.


1. The New Loading State: Automatic Suspense

The biggest shift with use() is that you no longer manage the loading state inside your component. Instead, you manage it around your component using Suspense.

When a component calls use(promise):

  1. If the Promise is pending, React doesn't crash or freeze; it suspends the component's rendering.
  2. React looks up the component tree for the nearest <Suspense> wrapper.
  3. It automatically displays the fallback UI provided in that wrapper.

Once the data is ready, the component rerenders and displays the final result.


2. Handling Errors: The Clean Error Boundary

What happens when your network call fails? You don't need the messy try...catch block inside your component anymore.

If the Promise passed to use() rejects (e.g., a 404 or 500 status), the error automatically "bubbles up" the component tree. It will be caught by the nearest Error Boundary.

Code Example: Error Handling

To catch and handle different error types (like a 404 vs a connection timeout), you simply ensure your Error Boundary component can inspect the thrown error object:

// App.jsx
<ErrorBoundary fallback={<GeneralErrorPage />}>
  <Suspense fallback={<UserSkeleton />}>
    <UserComponent userPromise={fetchUser(userId)} />
  </Suspense>
</ErrorBoundary>

Your custom ErrorBoundary component gets the raw error. You can check the error's properties (like error.status or error.message) to render specific messages:

// ErrorBoundary.jsx (Conceptual logic)
class ErrorBoundary extends React.Component {
  // ... boilerplate methods like getDerivedStateFromError
  render() {
    if (this.state.hasError) {
      if (this.state.error.status === 404) {
        return <h1>User Not Found! ️♀️</h1>;
      }
      return this.props.fallback; // General error page
    }
    return this.props.children;
  }
}

This separation of concerns, data fetching in the component, loading above, and errors above that makes our components much more focused and easier to maintain. It feels weird to let go of manual control, but the code is so much cleaner.

Have you tried refactoring your old fetching logic yet? Let me know what you think!

3 Comments

2 votes
1
2 votes
0
2 votes

More Posts

Stop Fetching Data in useEffect: You’re Not Writing jQuery Anymore

Sibasish Mohanty - Jul 23

Stop Fetching Data in useEffect: Redux Edition

Sibasish Mohanty - Jul 30

How I Started Learning React as a Beginner (My Honest 20-Day Journey)

Manoj Rahar - Nov 26

Stop Using “?” Everywhere! Use `Omit` (i.e. Utility Types) for Safer TypeScript

hossain45 - Nov 28

React Router Data Mode: Part 3 - Loaders and data fetching

Kevin Martinez - Jul 7
chevron_left