Beartropy Logo

The Ultimate Guide to Scalable Livewire Architecture: From Monoliths to Actions

Is your component 500 lines long? Learn how to refactor complex Livewire components using Form Objects, Action Classes, and Dependency Injection for a cleaner, scalable architecture.

Guides 05 Jan, 2026 Beartropy Team

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?

  1. Reusability: You can now create users from an API Controller, a CLI Command, or a Seeder using this exact same logic.
  2. 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

  1. Livewire is for UI State: Don't put business logic in it.
  2. Form Objects are Essential: They clean up your properties and validation rules.
  3. Actions are Reusable: They decouple your database logic from the HTTP request.
  4. 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.

Tags

#livewire #architecture #refactoring #laravel #best-practices #forms

Comments

Leave a comment

0

No comments yet. Be the first to share your thoughts!

Share this post