Skip to content

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.

const { start, clear, isRunning } = useTimeout(() => {
  showNotification();
}, 5000);

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} />;

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,
  };
}