React Hooks in SPFx: useCancelableFetch
Build a small, reusable hook around fetch and AbortController to keep your SPFx components safe from updates after unmount or rapid property changes.
API
import { useEffect, useState } from 'react';
export function useCancelableFetch<T>(url: string, deps: any[] = []) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const ac = new AbortController();
(async () => {
try {
setLoading(true);
setError(null);
const res = await fetch(url, { signal: ac.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
setData(json as T);
} catch (e: any) {
if (e.name !== 'AbortError') setError(e.message \u0000?? 'Unknown error');
} finally {
setLoading(false);
}
})();
return () => ac.abort();
}, deps);
return { data, loading, error };
}
Usage (SharePoint REST)
type TasksResponse = { value: { id: number; title: string }[] };
const { data, loading, error } = useCancelableFetch<TasksResponse>(
`${siteUrl}/_api/web/lists/getbytitle('Tasks')/items`,
[siteUrl]
);
if (loading) return <Spinner />;
if (error) return <MessageBar intent="error">{error}</MessageBar>;
return <ItemsList items={data?.value ?? []} />;
Usage (Microsoft Graph)
type UsersResponse = { value: { id: string; displayName: string }[] };
const { data, loading, error } = useCancelableFetch<UsersResponse>(
`${graphBase}/v1.0/users?$top=20`,
[graphBase]
);
Notes
- Always include dependencies like
siteUrlin the deps array. - Handles cancellation on unmount and between rapid prop changes.
- Keep the hook focused; for pagination or revalidation, extend with parameters or compose another hook.
See also
/blog/reactspfx/01_hooks_01_useState/blog/reactspfx/01_hooks_02_useEffect/blog/reactspfx/01_hooks_03_useRef/blog/reactspfx/01_hooks_04_useContext/blog/reactspfx/01_hooks_05_useReducer