skip to content

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?

level: juniorimportance: must knowfreq 48%

answer

  1. a trait on the component
  2. wire:model on the file input
  3. the property holds a temporary file
  4. validate, then store() or storeAs()
  5. never name a method upload

basics

~10 s

Add 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 s

Use 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
<?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

for a junior

Recall the trait, wire:model on the input, and store() or storeAs() in the save action.

for a middle

Explain the three-step temporary upload and that store() copies the file to its permanent disk.

for a senior

Plan naming, disks and cleanup so stored receipts are traceable and temporary files do not become the source of truth.

for a principal

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.