Custom Hooks¶
Reusable React hooks for common patterns.
Overview¶
flowchart TB
subgraph Data["Data/Image Hooks"]
A[useDebounce]
B[useFileCompression]
C[useInfiniteScroll]
D[useR2Upload]
E[useFileStatusCallback]
end
subgraph UI["UI Hooks"]
F[useTimeout]
G[useAutoUpdateOnIdle]
H[usePullToRefresh]
I[useDragHandleKeyboard]
J[useCaptcha]
end
subgraph Modal["Modal Hooks"]
L[useModal]
M[useEntityModal]
N[useFormModal]
O[useEntityDialogs]
end Data Hooks¶
useR2Upload¶
Direct upload to Cloudflare R2 with progress tracking.
const { upload, progress, isUploading } = useR2Upload();
const handleUpload = async (file: File) => {
const compressed = await compressImage(file);
const result = await upload(compressed, "markers");
return result.fileId;
};
Flow: 1. Compress image client-side 2. Request presigned URL from API 3. Upload directly to R2 4. Confirm upload with API 5. Poll for processing status
useFileStatusCallback¶
Poll file processing status until complete.
const { checkStatus } = useFileStatusCallback({
onComplete: (file) => console.log("Processed:", file),
onError: (error) => console.error(error),
});
await checkStatus(fileId);
useInfiniteScroll¶
Infinite scroll pagination with intersection observer.
const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteScroll({
queryKey: ["transactions"],
queryFn: fetchTransactions,
});
return (
<List>
{data.pages.flat().map(item => <Item key={item.id} />)}
<LoadMoreTrigger onIntersect={fetchNextPage} />
</List>
);
UI Hooks¶
useDebounce¶
Debounce value changes.
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 300);
useEffect(() => {
fetchResults(debouncedSearch);
}, [debouncedSearch]);
useTimeout¶
Timer with cleanup.
useAutoUpdateOnIdle¶
Auto-refresh data when user is idle.
useAutoUpdateOnIdle({
onUpdate: () => queryClient.invalidateQueries(["markers"]),
idleTime: 60000,
});
Input Hooks¶
usePullToRefresh¶
Mobile pull-to-refresh gesture.
const { containerRef, isPulling } = usePullToRefresh({
onRefresh: async () => {
await refetch();
},
});
return <div ref={containerRef}>...</div>;
useDragHandleKeyboard¶
Keyboard support for drag handles (accessibility).
const { onKeyDown } = useDragHandleKeyboard({
onMoveUp: () => moveItem(-1),
onMoveDown: () => moveItem(1),
});
return <DragHandle onKeyDown={onKeyDown} />;
Modal Hooks¶
useModal¶
Basic modal state management.
const { isOpen, open, close, toggle } = useModal();
return (
<>
<Button onClick={open}>Open</Button>
<Dialog open={isOpen} onClose={close}>...</Dialog>
</>
);
useEntityModal¶
CRUD modal for entities with form state.
const { isOpen, entity, openCreate, openEdit, close } = useEntityModal<Marker>();
return (
<>
<Button onClick={openCreate}>Add</Button>
<Button onClick={() => openEdit(marker)}>Edit</Button>
<MarkerDialog
open={isOpen}
marker={entity}
onClose={close}
/>
</>
);
useFormModal¶
Modal with form state and dirty tracking.
const { isOpen, isDirty, open, close, confirmClose } = useFormModal();
const handleClose = () => {
if (isDirty) {
confirmClose(); // Shows unsaved changes dialog
} else {
close();
}
};
Hook Composition¶
Hooks can be composed for complex behavior:
function useMarkerUpload() {
const { upload, progress } = useR2Upload();
const { checkStatus } = useFileStatusCallback();
return {
uploadMarkerImage: async (file: File) => {
const result = await upload(file, "markers");
await checkStatus(result.fileId);
return result;
},
progress,
};
}
Related¶
- API Layer — TanStack Query hooks
- State Management — Zustand stores
- Forms — Form handling