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:
- Reset cancellation state — Creates a fresh
AbortControllerand clears any previous mesh history viaclearMeshHistory(). - Initialize job record — Constructs a
GenerationJobobject with statusuploadingand stores it in the globalappStore. - Trigger backend upload — Calls
generateFromImagefromuseApito begin server-side processing. - Handle mid-upload cancellation — If the user cancels during upload, immediately calls
cancelJoband clears UI state. - Begin polling — Transitions status to
generatingand invokespollUntilDone(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— Setsprogress: 100, pushes the final mesh URL viapushMeshUrl, and exits.error— Records the error message and terminates polling.running/pending— Updates intermediate progress values including optionalstepdescriptions.
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
AbortControllerto terminate any in-flightfetchrequests.
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
pushMeshUrlandclearMeshHistory. - 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
startGenerationcreates jobs, clears history, uploads images, and initiates polling (lines 11-28).pollUntilDonerepeatedly checks status every second, handlingcancelled,done,error, and progress states (lines 61-94).cancelGenerationusescancelledRefandAbortControllerfor cooperative cancellation that cleans up both client and server state (lines 96-102).resetclears the current job locally without backend communication.- Zustand
appStoreserves 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →