**Approach**- Start with a simple component that fetches data and manages state.- Refactor fetching/state into a custom hook `useUser(userId)` so `UserProfile` becomes purely presentational.- Advantages: separation of concerns, easier unit testing of hook and UI separately, reuse.Initial (combined) version (TypeScript):tsx
import React, { useEffect, useState } from "react";
type User = { id: string; name: string; status: string };
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error("Fetch failed");
return res.json();
})
.then((data: User) => !cancelled && setUser(data))
.catch(err => !cancelled && setError(err.message))
.finally(() => !cancelled && setLoading(false));
return () => { cancelled = true; };
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.status}</p>
</div>
);
}
Refactored: custom hook + presentational componenttsx
import React from "react";
type User = { id: string; name: string; status: string };
export function useUser(userId: string) {
const [user, setUser] = React.useState<User | null>(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => { if (!res.ok) throw new Error("Fetch failed"); return res.json(); })
.then((data: User) => !cancelled && setUser(data))
.catch(err => !cancelled && setError(err.message))
.finally(() => !cancelled && setLoading(false));
return () => { cancelled = true; };
}, [userId]);
return { user, loading, error };
}
export function UserProfile({ userId }: { userId: string }) {
const { user, loading, error } = useUser(userId);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user</div>;
return (
<section>
<h2 data-testid="name">{user.name}</h2>
<p data-testid="status">{user.status}</p>
</section>
);
}
Why this is better- Testability: I can unit-test useUser by mocking fetch (or using msw) to assert state transitions, and separately snapshot/test UserProfile by supplying mocked props or by mocking the hook return. No need to mount network logic in UI tests.- Separation of concerns: Hook handles side effects and state; component focuses on rendering. This improves readability, reuse (other components can call useUser), and maintainability.- Predictability: Hook returns explicit loading/error/user states making error handling consistent across consumers.Edge cases & best practices- Abort fetch with AbortController for modern cancellation.- Retry/backoff or caching (SWR/React Query) for production.- Add PropTypes/strict TypeScript types and tests for loading, success, and error states.