How the `useGeneration` Hook Manages the Generation Lifecycle in Modly

The useGeneration hook orchestrates the complete lifecycle of a 3-D generation job in Modly through three coordinated concerns: job creation and upload, polling with progress updates, and cancellation with reset capabilities.

The useGeneration hook serves as the central nervous system for 3-D mesh generation in the Modly application. Located at src/shared/hooks/useGeneration.ts, it bridges React components with the FastAPI backend, managing state transitions from initial upload through final mesh delivery. This hook demonstrates a production-ready pattern for handling long-running asynchronous operations with cancellation support.

Hook Initialization and Dependencies

The hook begins by extracting state and API functions from two critical sources. From the Zustand appStore, it retrieves the current job state, update methods, and mesh history management. From useApi, it obtains the three API functions that communicate with the backend.

const { currentJob, setCurrentJob, updateCurrentJob, generationOptions,
        selectedImageData, pushMeshUrl, clearMeshHistory } = useAppStore()
const { generateFromImage, pollJobStatus, cancelJob } = useApi()

This dependency injection pattern (lines 5-8 in useGeneration.ts) ensures the hook remains testable and decoupled from direct API implementation details.

Starting a Generation Job

The startGeneration function (lines 11-28) initiates the lifecycle through a carefully sequenced five-step process:

  1. Reset cancellation state — Creates a fresh AbortController and clears any previous mesh history via clearMeshHistory().
  2. Initialize job record — Constructs a GenerationJob object with status uploading and stores it in the global appStore.
  3. Trigger backend upload — Calls generateFromImage from useApi to begin server-side processing.
  4. Handle mid-upload cancellation — If the user cancels during upload, immediately calls cancelJob and clears UI state.
  5. Begin polling — Transitions status to generating and invokes pollUntilDone(jobId).
export function GenerateButton({ imagePath }: { imagePath: string }) {
  const { currentJob, startGeneration, cancelGeneration, reset } = useGeneration()

  return (
    <>
      {currentJob?.status === 'generating' ? (
        <button onClick={cancelGeneration}>Cancel</button>
      ) : (
        <button onClick={() => startGeneration(imagePath)}>Generate</button>
      )}
      {currentJob?.status === 'error' && (
        <div>
          <p>Error: {currentJob.error}</p>
          <button onClick={reset}>Try again</button>
        </div>
      )}
    </>
  )
}

The Polling Loop: pollUntilDone

The heart of the generation lifecycle resides in pollUntilDone (lines 61-94), which implements a robust polling mechanism with four distinct status handlers:

  • cancelled — Clears the UI state and terminates the loop.
  • done — Sets progress: 100, pushes the final mesh URL via pushMeshUrl, and exits.
  • error — Records the error message and terminates polling.
  • running / pending — Updates intermediate progress values including optional step descriptions.

The loop executes every 1000 milliseconds using setTimeout, checking the cancelledRef flag before each iteration. This cooperative cancellation pattern prevents race conditions between user-initiated cancellation and in-flight status requests.

export function GenerationProgress() {
  const { currentJob } = useGeneration()
  if (!currentJob) return null

  return (
    <div>
      <p>Status: {currentJob.status}</p>
      <p>Progress: {currentJob.progress}%</p>
      {currentJob.step && <p>Step: {currentJob.step}</p>}
    </div>
  )
}

Cancellation and Abort Handling

The cancelGeneration function (lines 96-102) implements immediate cooperative cancellation through two synchronized mechanisms:

  • Flag-based cancellation — Sets cancelledRef.current = true, which the polling loop inspects on its next iteration.
  • Request abortion — Signals the AbortController to terminate any in-flight fetch requests.

When the polling loop detects the cancellation flag, it proactively calls cancelJob to notify the backend, then clears the UI state. This ensures server resources are freed even if the user abandons the operation mid-generation.

Reset: Returning to Idle State

The reset function provides a simple escape hatch: it calls setCurrentJob(null) to clear the current job from the appStore, immediately returning all consuming components to their idle state. Unlike cancellation, reset does not communicate with the backend—it purely affects client-side state.

State Management Architecture

All mutations flow through the Zustand store defined in src/shared/stores/appStore.ts. This centralized state management ensures:

  • Components re-render automatically when job status changes.
  • Mesh history persists across generation sessions via pushMeshUrl and clearMeshHistory.
  • Generation options remain synchronized between the UI and active jobs.

The hook never mutates local React state directly—all updates route through setCurrentJob and updateCurrentJob store methods.

API Integration Layer

The useApi hook (src/shared/hooks/useApi.ts) encapsulates three FastAPI endpoints:

Endpoint Function Purpose
POST /generate/from-image generateFromImage Upload image and create job
GET /generate/status/:id pollJobStatus Retrieve current job status and progress
POST /generate/cancel/:id cancelJob Request server-side cancellation

This abstraction allows useGeneration to remain agnostic of HTTP implementation details while maintaining type safety across the API boundary.

Summary

  • startGeneration creates jobs, clears history, uploads images, and initiates polling (lines 11-28).
  • pollUntilDone repeatedly checks status every second, handling cancelled, done, error, and progress states (lines 61-94).
  • cancelGeneration uses cancelledRef and AbortController for cooperative cancellation that cleans up both client and server state (lines 96-102).
  • reset clears the current job locally without backend communication.
  • Zustand appStore serves as the single source of truth for all generation state.

Frequently Asked Questions

How does useGeneration handle network failures during polling?

The hook treats any polling failure as an error condition. When pollJobStatus throws or returns an unexpected response, the polling loop captures the error message via updateCurrentJob({ status: 'error', error: message }) and terminates. The UI can then display the error and offer a reset option.

Can multiple generation jobs run simultaneously?

No—useGeneration manages a singleton currentJob. Starting a new generation calls clearMeshHistory(), wiping previous results. For concurrent generations, the hook would require architectural changes to track a job map rather than a single job reference.

What prevents memory leaks from the polling interval?

The pollUntilDone loop uses setTimeout rather than setInterval, ensuring only one pending timer exists at any moment. The AbortController and cancelledRef pattern guarantees the loop exits cleanly on unmount or cancellation, preventing orphaned timers.

How does cancellation synchronize between client and server?

The client sets a local flag immediately for responsive UI feedback. The next polling iteration detects this flag and calls cancelJob, which sends a cancellation request to the FastAPI backend. The server then updates the job status to cancelled, which the subsequent poll confirms before clearing state.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →