In a Livewire component, how do you let a user attach a receipt file and then store it permanently when the expense claim is saved?
answer
- a trait on the component
- wire:model on the file input
- the property holds a temporary file
- validate, then store() or storeAs()
- never name a method upload
basics
~10 sAdd the WithFileUploads trait, bind the file input with wire:model="receipt", and in the save action validate $this->receipt and call $this->receipt->store('receipts') or storeAs(), which returns the stored path.
solid answer
~40 sUse the `WithFileUploads` trait and bind `<input type="file" wire:model="receipt">`. As soon as a file is chosen, Livewire uploads it to a signed temporary endpoint, stores it in a temporary directory and sets `$receipt` to a `TemporaryUploadedFile`. The upload is not yet permanent: in the `save()` action you validate it, then call `$this->receipt->store('receipts')` for a generated hash name, `storeAs('receipts', $name)` for your own name, or pass a disk such as `store('receipts', 's3')`; each returns the stored path, which you save on the claim. Two gotchas: without the trait Livewire throws `MissingFileUploadsTraitException`, and `upload` is a reserved name, so the action must be called something like `save`.
code
php · 22 lines<?php
use App\Models\ExpenseClaim;
use Livewire\Component;
use Livewire\WithFileUploads;
new class extends Component {
use WithFileUploads;
public ExpenseClaim $claim;
public $receipt;
public function save(): void
{
$this->validate(['receipt' => 'required|file|mimes:pdf,jpg,png|max:5120']);
$path = $this->receipt->store('receipts');
$this->claim->receipts()->create(['path' => $path]);
$this->reset('receipt');
}
};go deeper
Recall the trait, wire:model on the input, and store() or storeAs() in the save action.
Explain the three-step temporary upload and that store() copies the file to its permanent disk.
Plan naming, disks and cleanup so stored receipts are traceable and temporary files do not become the source of truth.
Decide where user files live and how long, balancing audit needs for receipts against storage cost and privacy.
## Why Livewire needs its own upload flow A **Livewire** component talks to the server with JSON update requests carrying its state. A file cannot sensibly ride inside that JSON, so Livewire uploads files **separately** and then hands the component a reference. The trait `Livewire\WithFileUploads` turns this on; with it, a file input can use `wire:model` like any other input. ## What happens when the user picks a receipt 1. Livewire's JavaScript asks the component for a **signed upload URL**. 2. It posts the file to that URL; Livewire's upload controller validates it against global rules and stores it in a **temporary directory** (`livewire-tmp` by default). 3. It sends a final component request that sets the bound property to the new temporary file. 4. The property, `$receipt`, now holds a `Livewire\Features\SupportFileUploads\TemporaryUploadedFile`, a subclass of Laravel's `UploadedFile`. All of this happens when the file is selected, **before** the user clicks Save. That is why the file can be previewed and validated early. ## Storing it for real The temporary file is not the final home. In the action that saves the claim: | Call | Result | |---|---| | `$this->receipt->store('receipts')` | copied to `receipts/` on the default disk with a random hash name | | `$this->receipt->store('receipts', 's3')` | same, on the `s3` disk | | `$this->receipt->storeAs('receipts', "claim-{$id}.pdf")` | copied with the name you choose | | `$this->receipt->storePublicly('receipts', 's3')` | stored with public visibility | Each returns the **stored path**, which is what you persist on the expense claim record. `getClientOriginalName()` gives the name the user's file had, useful for display but not as a storage path. ## Multiple receipts Add `multiple` to the input and bind an array property: `<input type="file" wire:model="receipts" multiple>` with `public $receipts = [];`. Livewire appends each uploaded file to the array, and you loop over it to store each one. ## Gotchas - **Missing trait.** Starting an upload on a component without `WithFileUploads` throws `MissingFileUploadsTraitException`. - **Reserved name.** `upload` is used internally; a method or property called `upload` on the component breaks uploads, so call the action `save` or `submit`. - **Validate before storing.** The temporary file has passed only Livewire's global checks, not your rules for receipts. - **Clear after saving.** Reset the property after storing so the form does not keep pointing at the temporary file. ## Choosing a file name `store()` generates a random 40-character hash name with the original extension, which is collision-free and hides the user's file name. `storeAs()` is better when the name must be predictable, such as `claims/{$claim->id}/receipt-1.pdf`, so that support staff can find a receipt from the claim number. Either way the original name belongs in a database column for display, not in the path, because it is chosen by the user and may contain spaces, unicode or characters your storage treats specially. ## Where the pieces live | Piece | Lives in | |---|---| | the trait `WithFileUploads` | the component class | | `wire:model="receipt"` | the file input in the view | | temporary file | `livewire-tmp` on the temporary disk | | permanent file | the disk and path you pass to `store()` | | the path you keep | a column on the expense claim or its receipts table | Keeping this map in mind prevents the most common confusion: treating the property as if it were the stored file. It is a handle on a temporary upload for the duration of the form, nothing more. ## What an interviewer is checking The junior-level answer is the trait, `wire:model` and `store()`. A stronger answer explains that the file is uploaded **on selection** to a temporary location and that `store()` is what makes it permanent, which is the model the rest of Livewire's upload features build on: validation of the temporary file, previews, direct-to-cloud uploads and progress events.
- Why does the file start uploading before the user clicks Save?`wire:model` on a file input triggers Livewire's upload flow on selection: it fetches a signed URL, uploads to a temporary directory and sets the property. That lets the form validate or preview the file early, and the Save action only has to copy it to its final place.
- What does store() return, and what should you save on the expense claim?It returns the path of the stored file relative to the disk, such as `receipts/abc123.pdf`. Save that path, plus the disk if more than one is used; do not save the temporary file's path, which is cleaned up later.
saying these in an interview costs you the question
- Believes the file is uploaded only when the form is submitted.
- Treats the temporary file's location as the permanent storage path.
- Names the component action upload().
- Uses getClientOriginalName() directly as the stored file path.
- Stores the file before validating it.