livewire/livewire: Documenting crop-before-upload workflows: what PR #10712 adds to the v4 upload docs
Contributed by calebporzio
Part of the livewire v4.4.7 release
Pull request #10712 is a documentation-only change, but it targets one of the most common frictions in Livewire's upload flow: cropping or resizing an image before anything leaves the browser. It adds a new section — Processing images before uploading — to the v4.x upload documentation (docs/uploads.md). The motivation was a developer-reported pain point: with wire:model on a file input, selecting a photo uploads the untouched original immediately, which is exactly wrong for a crop-first workflow like a profile-picture editor. The new docs describe the supported pattern for deferring the upload until the user confirms a crop. If you are upgrading from v3.8.10, there is no behavior change and no code to migrate — this is guidance, not a framework change.
What changed
The upload guide gains a section between the JavaScript hook examples and the Configuration chapter. It covers, concisely:
- Why a
wire:modelfile input uploads the moment a file is selected, and why that default fights crop-before-upload workflows. - Keeping the source input unbound, so picking, zooming, discarding, and re-selecting all stay purely local.
- Exporting the crop to a canvas, wrapping the blob in a named
File, and calling$wire.upload()from the editor's confirmation handler. - Calling
save()only from the upload's success callback, wrapping the editor inwire:ignore, and disabling the confirm button to prevent duplicate submissions.
The pattern in code
The input deliberately has no binding:
{{-- No wire:model here: selection must not trigger an upload --}}
<input type="file" accept="image/*">
In the cropper's confirm handler, export and upload the result:
let canvas = await cropper.getCropperSelection().$toCanvas({
width: 256,
height: 256,
})
canvas.toBlob((blob) => {
if (! blob) {
alert('The cropped image could not be exported. Please try again.')
return
}
let file = new File([blob], 'avatar.png', { type: 'image/png' })
$wire.upload('photo', file, () => {
$wire.save()
}, () => {
alert('The upload failed. Please try again.')
})
}, 'image/png')
The component side is unchanged from what you already know: WithFileUploads, a $photo property, and a save() action that validates and stores the image. Wrap the editor's generated markup so Livewire's morphing leaves it alone:
<div wire:ignore>
<!-- cropper-generated markup -->
</div>
Why it matters
Under the default behavior, the full original — a 900×600 photo in the PR's own test — is POSTed to your temporary upload disk the instant it is chosen, before the user has cropped anything and even if they cancel. That costs bandwidth, leaves unused files behind for cleanup, and can be a privacy surprise when your temporary disk is S3 and originals reach the cloud regardless. The documented pattern inverts this: the original never leaves the browser, and the only thing transmitted is the confirmed 256×256 crop, exactly when the user says so. Just as valuable, this is now an officially documented approach with a maintained example, instead of something each team assembles from forum posts.
Details that are easy to miss
wire:ignoreis required hygiene. Livewire's morphing would otherwise clobber the DOM your cropper generated around the input.- Guard against double submits. Disable the confirmation button while the canvas is exporting and the upload is in flight; an impatient second click otherwise uploads twice.
$wire.upload()does not return a promise. You cannotawaitit — pass success and error callbacks, and trigger$wire.save()from the success callback so$photois guaranteed to be set before validation and storage run.- Name your file.
new File([blob], 'avatar.png', { type: 'image/png' })gives the blob a filename and MIME type, which matters forimagevalidation and for storage paths.
How it was verified
The author validated the documented flow in a real Laravel app running Livewire v4.4.5 and Cropper.js 2.1.0, driven through Chrome via Playwright. The bound control uploaded the 900×600 original immediately; the unbound flow produced zero POST requests through local selection, zoom, discard, and re-selection; confirmation uploaded and persisted only the 256×256 crop; an aborted upload saved nothing, and a retry then succeeded; the stored image rendered at 256×256 with no console exceptions.
What this means for your v3.8.10 → v4 upgrade
Nothing breaks and nothing needs rewriting. $wire.upload(name, file, onSuccess, onError) behaves the same as before, and WithFileUploads, wire:ignore, and the temporary-upload mechanics carry over. If you already built a cropper in v3, you were likely using this unbound-input pattern already — and it keeps working. The real upgrade value is that the v4 docs now sanction the approach and document its sharp edges, so it is worth a quick re-read to compare against your implementation, particularly your error callback and duplicate-submission guard.
Takeaways:
- Documentation-only PR: new "Processing images before uploading" section in the v4.x upload docs; no framework behavior changed.
- Omit
wire:modelon the source input; upload the processed file with$wire.upload()when the user confirms. - Wrap editor markup in
wire:ignoreand disable the confirm button during export/upload. $wire.upload()uses callbacks, not a promise; call your save action from the success callback.- Verified end to end: only the confirmed crop ever left the browser.