![]()
Handling API Response Data Before It Loads
A common React mistake is trying to access API response data before it’s available, leading to “cannot read property of undefined” errors. Here’s how to properly handle asynchronous data loading.
The Problem (Premature Data Access)
// ❌ Wrong - Accessing data before it loads
function UserProfile() {
const [user, setUser] = useState(); // undefined by default
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data));
}, []);
return (
<div>
<h1>{user.name}</h1> {/* ❌ TypeError if user is undefined */}
<p>{user.email}</p>
</div>
);
}
Correct Solutions
1. Initialize State with Proper Structure
function UserProfile() {
// ✅ Initialize with expected structure
const [user, setUser] = useState({
name: '',
email: '',
loading: true
});
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser({ ...data, loading: false }));
}, []);
if (user.loading) return <Spinner />;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
2. Optional Chaining Operator
function UserProfile() {
const [user, setUser] = useState();
useEffect(() => { /* fetch logic */ }, []);
return (
<div>
<h1>{user?.name}</h1> {/* ✅ Safe access with ?. */}
<p>{user?.email || 'Loading...'}</p> {/* ✅ Fallback value */}
</div>
);
}
3. Loading and Error States
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(res => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
if (!user) return <div>No user data</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Common Mistakes to Avoid
- No loading state:
const [data, setData] = useState(); // ❌ No indication of loading state
- Assuming successful fetch:
fetch('/api').then(setData); // ❌ No error handling
- Nested object access without checks:
<div>{user.profile.address.city}</div> // ❌ Multiple potential undefineds
- Empty dependency arrays with closures:
useEffect(() => {
fetch(`/api/user/${userId}`); // ❌ Missing userId dependency
}, []);
Best Practices
- Always initialize state with proper structure or null
- Track loading/error states explicitly
- Use optional chaining for nested access:
user?.profile?.address - Provide fallback UI during loading
- Handle errors gracefully – don’t ignore failed requests
- TypeScript users – Define proper types:
interface User {
name: string;
email: string;
profile?: {
address?: {
city: string;
}
}
}
const [user, setUser] = useState<User | null>(null);
Advanced Patterns
1. Custom Hook with Status
function useFetch(url) {
const [state, setState] = useState({
data: null,
loading: true,
error: null
});
useEffect(() => {
setState(s => ({ ...s, loading: true }));
fetch(url)
.then(res => res.json())
.then(data => setState({ data, loading: false, error: null }))
.catch(error => setState({ data: null, loading: false, error }));
}, [url]);
return state;
}
// Usage
const { data: user, loading, error } = useFetch('/api/user');
2. Suspense with Error Boundaries
// Using React 18's Suspense
const resource = fetchUserData(); // Returns a promise with .read() method
function UserProfile() {
const user = resource.read(); // Will suspend until data loads
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// In your app
<Suspense fallback={<Spinner />}>
<ErrorBoundary>
<UserProfile />
</ErrorBoundary>
</Suspense>
3. Using Libraries (React Query Example)
import { useQuery } from 'react-query';
function UserProfile() {
const { data: user, isLoading, error } = useQuery('user', () =>
fetch('/api/user').then(res => res.json())
);
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Remember: Always assume network requests are asynchronous and may fail. Your components should handle:
- Loading states – Show spinners or placeholders
- Empty states – When no data exists
- Error states – When requests fail
- Partial data – When some fields might be missing
Proper data loading handling improves user experience and makes your application more resilient.
