Define the backend contract
The backend should create or accept an upload job with a stable identifier. The interface needs to distinguish:
selected -> queued -> uploading -> verifying -> received
| |
v v
failed rejected
“100% transferred” is not necessarily “received.” The server may still verify size, type, checksum, authorization, scanning, or storage commitment.
The W3C File API defines browser file objects and related interfaces. It does not define a secure application upload protocol, resumability, or server acceptance.
Put state above the modal
React associates state with a component's position in the tree, as explained in preserving and resetting state. If the modal owns the upload state and is removed, that state is removed with it.
Use a stable route-level or application-level upload manager:
const uploadJob = {
id: "up_local_1",
fileName: "report.pdf",
bytesTotal: 120000,
bytesSent: 0,
status: "queued",
serverUploadId: null,
error: null,
};
The modal adds a job, then closes. A persistent upload tray renders jobs from the manager. A server-state library or context may host this model; the ownership boundary matters more than the library.
Keep transport handles out of render data
An abort controller, stream, or multipart client is operational state. Store it in a manager keyed by upload ID rather than serializing it into business data. Expose commands such as:
enqueue(file, metadata)
cancel(uploadId)
retry(uploadId)
removeCompleted(uploadId)
The JSX is not provided here because transport choice determines event and cleanup behavior. The state model is illustrative and requires integration testing.
Design modal and tray behavior
The modal:
- validates only enough to give immediate feedback;
- adds the job once;
- announces that upload continues after closing; and
- returns focus predictably.
The persistent tray:
- uses the stable upload ID as its React key;
- shows file name, status, and meaningful progress;
- offers cancel only when cancellation is supported;
- distinguishes retryable failure from server rejection;
- announces completion or failure in a status region; and
- links the received file to the resulting business record.
Do not expose local paths or unnecessary file metadata.
Test lifecycle and recovery
Test:
- select a file, close the modal, and observe continued progress;
- reopen the modal without creating a duplicate job;
- navigate within the application while the manager remains mounted;
- cancel and verify both transport and server cleanup;
- fail midway and retry according to the backend protocol;
- reach full transfer but fail server verification;
- complete and reconcile the authoritative file identifier; and
- refresh the browser and show an honest status for an in-flight job.
A browser refresh may require a resumable server protocol; local React state alone cannot preserve an active transfer.
State what remains unproved
This pattern does not secure file uploads. The backend must enforce current authorization, tenant scope, filename policy, size and type rules, storage, retention, safe retrieval, and any required scanning. The UI owns visibility and recovery; the server owns acceptance.