As developers, we know that user experience is paramount. In web applications, this often means providing clear feedback to users about what's happening, especially when fetching data. In Next.js, handling loading and error states for your data fetches is straightforward and crucial for a smooth user journey. This section will guide you through common patterns and best practices.
When data is being fetched, it's good practice to show a loading indicator. This lets the user know that the application is working to retrieve the information they requested and prevents them from thinking the page is broken. In Next.js, especially with client-side fetching, you can leverage component state to manage this.
import { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch('/api/mydata');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>My Data</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
export default MyComponent;The useEffect hook is our workhorse here. We initialize loading to true. Inside the useEffect, we set up an asynchronous function fetchData. We wrap the fetch logic in a try...catch...finally block. The try block attempts to fetch the data and update the data state. If an error occurs during the fetch, the catch block updates the error state. Regardless of success or failure, the finally block ensures setLoading(false) is always called, signaling the end of the loading period.
In the rendering part of our component, we first check the loading state. If it's true, we display a 'Loading...' message. If loading is false and there's an error, we display an error message. Only if both loading and error are false do we render the actual data.
For server-side rendering (SSR) or static site generation (SSG) with getStaticProps or getServerSideProps, Next.js provides a slightly different mechanism. These functions run on the server (or at build time) and can return props to your page component. If an error occurs within these functions, Next.js will typically show an error page by default. However, you can handle errors within these functions and decide what to return.
export async function getServerSideProps(context) {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
// Handle non-OK responses
return {
props: { data: null, error: `Failed to fetch data: ${response.status}` }
};
}
const data = await response.json();
return { props: { data, error: null } };
} catch (error) {
// Handle network or other errors
return { props: { data: null, error: error.message } };
}
}
function MyPage({ data, error }) {
if (error) {
return <div>Error loading data: {error}</div>;
}
return (
<div>
<h1>Server-Rendered Data</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}In this SSR example, getServerSideProps attempts to fetch data. If the response is not ok or if a catch block catches an error, we return props that include an error message. The page component then checks for this error prop and displays a user-friendly message if it exists. This approach ensures that even server-side fetches provide feedback.
For a more sophisticated loading experience, especially with client-side navigation in Next.js, you can leverage the Router's events. The useRouter hook provides access to router events like routeChangeStart and routeChangeComplete which can be used to globally manage loading states, perhaps by showing a top-progress bar.
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';
function AppLayout({ children }) {
const router = useRouter();
const [loading, setLoading] = useState(false);
useEffect(() => {
const handleStart = () => setLoading(true);
const handleComplete = () => setLoading(false);
router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleComplete);
return () => {
router.events.off('routeChangeStart', handleStart);
router.events.off('routeChangeComplete', handleComplete);
};
}, [router.events]);
return (
<div>
{loading && <div className="progress-bar">Loading...</div>}
{children}
</div>
);
}
export default AppLayout;This example shows how to set up a global loading indicator using router.events. When a route change starts (routeChangeStart), we set loading to true. When the route change completes (routeChangeComplete), we set loading back to false. This provides a visual cue during client-side navigation, which is a common pattern in single-page applications built with Next.js. Remember to clean up event listeners in the useEffect's return function.
Finally, it's important to think about the user's perspective. Loading states shouldn't just be silent indicators; they should be informative. Instead of a generic 'Loading...', consider showing what's being loaded, e.g., 'Loading user profile...' or 'Fetching product details...'. Similarly, error messages should be clear and actionable, guiding the user on what to do next, if possible.
graph TD;
A[Start] --> B{Is Data Fetching?};
B -- Yes --> C[Show Loading Indicator];
C --> D{Data Fetched?};
B -- No --> E[Show Data];
D -- Yes --> E;
D -- No --> F{Error Occurred?};
F -- Yes --> G[Show Error Message];
F -- No --> D;
G --> H[End];
E --> H;