Livewire makes it deceptively easy to build dynamic interfaces. But ease of use often leads to a common architectural trap: The Fat Component.
We have all been there. You start with a simple form. Then you add validation. Then a database transaction. Then an email notification. Then a third-party API call.
Suddenly, your CreateUser.php component is 600 lines long, impossible to test, and terrifying to refactor.
In this extensive guide, we are going to dismantle a monolithic component and rebuild it using a professional, scalable architecture suitable for enterprise applications in 2026.
🍝 Phase 1: The Problem (The Spaghetti Monolith)
Let's look at a typical component found in many projects. It handles UI state, data validation, business logic, and side effects all in one place.
1class CreateUser extends Component
2{
3 // ❌ Property Pollution
4 public $name;
5 public $email;
6 public $password;
7 public $role = 'user';
8 public $send_welcome_email = true;
9
10 public function save()
11 {
12 // ❌ Validation Logic mixed with Business Logic
13 $this->validate([
14 'name' => 'required|min:3',
15 'email' => 'required|email|unique:users',
16 'password' => 'required|min:8',
17 ]);
18
19 // ❌ Database Logic inside the Controller
20 DB::beginTransaction();
21 try {
22 $user = User::create([
23 'name' => $this->name,
24 'email' => $this->email,
25 'password' => Hash::make($this->password),
26 'role' => $this->role,
27 ]);
28
29 // ❌ Side Effects (Mailing) blocking the request
30 if ($this->send_welcome_email) {
31 Mail::to($user)->send(new WelcomeEmail($user));
32 }
33
34 DB::commit();
35
36 // ❌ Hardcoded UI Feedback
37 $this->dispatch('notify', 'User created!');
38 $this->reset();
39
40 } catch (\Exception $e) {
41 DB::rollBack();
42 $this->addError('email', 'System error: ' . $e->getMessage());
43 }
44 }
45
46 public function render()
47 {
48 return view('livewire.create-user');
49 }
50}
This works, but it violates the Single Responsibility Principle. This component knows too much.
🛠 Phase 2: Extracting State (Livewire Forms)
First, let's move the data structure and validation rules out of the component. Livewire 3 introduced dedicated Form Objects for this exact purpose.
Create app/Livewire/Forms/UserForm.php:
1namespace App\Livewire\Forms;
2
3use Livewire\Attributes\Validate;
4use Livewire\Form;
5
6class UserForm extends Form
7{
8 #[Validate('required|min:3')]
9 public $name = '';
10
11 #[Validate('required|email|unique:users')]
12 public $email = '';
13
14 #[Validate('required|min:8')]
15 public $password = '';
16
17 #[Validate('required|in:admin,user,editor')]
18 public $role = 'user';
19
20 public $send_welcome_email = true;
21}
Now, our component shrinks significantly. We simply inject the form:
1class CreateUser extends Component
2{
3 public UserForm $form;
4
5 public function save()
6 {
7 $this->form->validate();
8
9 // The business logic is still here, but the state is clean.
10 // ...
11 }
12}
⚡ Phase 3: Extracting Logic (Action Classes)
A Controller (or Livewire Component) should orchestrate, not execute. The logic of "Creating a User" belongs in the domain layer, not the HTTP layer.
Let's create a single-purpose Action Class: app/Actions/CreateUserAction.php.
1namespace App\Actions;
2
3use App\Models\User;
4use Illuminate\Support\Facades\DB;
5use Illuminate\Support\Facades\Hash;
6
7class CreateUserAction
8{
9 public function execute(array $data, bool $sendEmail): User
10 {
11 return DB::transaction(function () use ($data, $sendEmail) {
12 $user = User::create([
13 'name' => $data['name'],
14 'email' => $data['email'],
15 'password' => Hash::make($data['password']),
16 'role' => $data['role'],
17 ]);
18
19 if ($sendEmail) {
20 // Best Practice: Dispatch a Job, don't block the UI
21 \App\Jobs\SendWelcomeEmail::dispatch($user);
22 }
23
24 return $user;
25 });
26 }
27}
Why is this better?
- Reusability: You can now create users from an API Controller, a CLI Command, or a Seeder using this exact same logic.
- Testing: You can unit test the
CreateUserAction without booting up Livewire.
💎 Phase 4: The Final Composition
Now, let's look at our refactored Livewire component. It acts purely as a bridge between the User Interface (Beartropy UI) and your Application Core.
1namespace App\Livewire;
2
3use App\Livewire\Forms\UserForm;
4use App\Actions\CreateUserAction;
5use Livewire\Component;
6use Beartropy\Alerts\Traits\HasToast; // Assuming you use Beartropy Alerts
7
8class CreateUser extends Component
9{
10 use HasToast;
11
12 public UserForm $form;
13
14 // Dependency Injection works in Livewire actions!
15 public function save(CreateUserAction $creator)
16 {
17 $this->form->validate();
18
19 try {
20 $creator->execute(
21 $this->form->all(),
22 $this->form->send_welcome_email
23 );
24
25 $this->toast()->success('User created successfully!');
26 $this->dispatch('user-created');
27 $this->form->reset();
28
29 } catch (\Exception $e) {
30 // Log internally, show generic message to user
31 report($e);
32 $this->toast()->error('Something went wrong. Please try again.');
33 }
34 }
35
36 public function render()
37 {
38 return view('livewire.create-user');
39 }
40}
🖼️ The View: Clean and Semantic
Finally, your Blade view becomes a declarative description of your UI, powered by Beartropy components binding directly to the Form Object.
1<form wire:submit="save" class="space-y-6">
2 <x-bt-card title="Create New User">
3
4 <div class="grid grid-cols-2 gap-6">
5 <x-bt-input
6 label="Full Name"
7 wire:model="form.name"
8 placeholder="John Doe"
9 />
10
11 <x-bt-input
12 label="Email Address"
13 wire:model="form.email"
14 type="email"
15 />
16 </div>
17
18 <x-bt-select
19 label="Role"
20 wire:model="form.role"
21 :options="['user' => 'Standard User', 'admin' => 'Administrator']"
22 />
23
24 <div class="mt-4">
25 <x-bt-toggle
26 label="Send Welcome Email"
27 description="User will receive login instructions instantly."
28 wire:model="form.send_welcome_email"
29 />
30 </div>
31
32 <x-slot:footer>
33 <div class="flex justify-end">
34 <x-bt-button type="submit" primary loading="save">
35 Create User
36 </x-bt-button>
37 </div>
38 </x-slot:footer>
39
40 </x-bt-card>
41</form>
Key Takeaways for 2026
- Livewire is for UI State: Don't put business logic in it.
- Form Objects are Essential: They clean up your properties and validation rules.
- Actions are Reusable: They decouple your database logic from the HTTP request.
- Beartropy UI Binds Deeply: Components like
x-bt-input work seamlessly with nested Form Objects (form.name).
Start refactoring your biggest component today using this pattern, and you will see how much easier your application becomes to maintain.