In this second part, I’ll walk you through the component architecture behind this solution — how I structured the Livewire components, delegated logic to services, and enabled event-driven communication across the system.
Livewire 3 in the Presentation Layer
It’s important to understand that Livewire 3 operates in the presentation layer of your application. It’s responsible for:
• Handling the user interface
• Synchronizing data with the backend
• Updating the page reactively without reloading the entire page
Common Pitfall: Bloated Components
Many developers mistakenly treat Livewire components as a place for business logic or data access.
While Livewire can directly access models and repositories, this leads to bloated, hard-to-maintain components.
Instead, treat Livewire as the bridge between the browser and your application logic:
Livewire (presentation layer)
- Receives data
- Renders views
- Validates inputs
- Dispatches events
Application layer (services, DTOs, queries)
- Handles data fetching
- Processes business rules
- Performs calculations
Key Components
The system consists of three Livewire 3 components:
1️⃣ Filters
– manages filter UI, SEO meta tags, and filter payload
2️⃣ Products
– handles fetching product data and pagination
3️⃣ ProductsSummarize
– displays a summary of the product count and reacts to product events
Filters
: Responsibilities
The Filters
component:
- Parses filters from the current URL
- Loads available filter options for the selected category
- Maps selected filter values from the query
- Transforms raw filters into a frontend-friendly structure
- Dispatches SEO meta tag updates (title, description, canonical, robots)
Main Dependencies
public function boot(
CategoryFiltersResolver $categoryFiltersResolver,
FilterStateMapper $filterStateMapper,
UrlFiltersParser $urlFiltersParser,
FiltersToViewDataTransformer $viewDataTransformer,
CurrentUrlFilterState $currentUrlFilterState,
PageSeoDataProvider $pageSeoDataProvider
)
Each service has a single responsibility:
Service | Responsibility |
---|---|
CategoryFiltersResolver |
Returns available filters for a given category |
FilterStateMapper |
Maps selected filters from query parameters |
UrlFiltersParser |
Extracts and sanitizes filter query groups from the URL |
FiltersToViewDataTransformer |
Converts raw filters into frontend-ready data |
CurrentUrlFilterState |
Holds current route and query state |
PageSeoDataProvider |
Generates SEO meta data: title, description, robots, canonical |
Note:
PageSeoDataProvider
plays a key role in this architecture:
It ensures that all SEO-related data — meta titles, H1 titles, robots index/noindex rules — are dynamically calculated on the server. These values adapt to the current filter state, the selected country, and other business-specific conditions, guaranteeing accurate and SEO-optimized output for each filter combination.
Products
: Responsibilities
The Products
component:
- Listens for events like
filter-updated
,page-changed
, andsort-updated
- Fetches the filtered product list and pagination data
- Dispatches
products-updated
events to inform other components
Main Dependencies
public function boot(
ProductsProvider $productsProvider,
Pagination $paginationService,
UrlFiltersParser $urlFiltersParser,
LocalizationInfo $localizationInfo,
CurrentUrlFilterState $currentUrlFilterState
)
Service | Responsibility |
---|---|
ProductsProvider |
Fetches the filtered product collection |
Pagination |
Builds pagination structure for the current state |
UrlFiltersParser |
Parses the current URL query string |
LocalizationInfo |
Provides locale and country context |
CurrentUrlFilterState |
Holds the current route and query parameters |
ProductsSummarize
: Responsibilities
The ProductsSummarize
component is purely presentational:
- It listens for
products-loaded
andproducts-updated
events - It updates the summary bar in real-time based on those events
#[On('products-loaded')]
#[On('products-updated')]
public function updateSummarize(int $foundProducts, int $totalInCategory): void
{
$this->foundProducts = $foundProducts;
$this->totalProductsInCategory = $totalInCategory;
}
Event-Driven Communication
My components don’t depend directly on each other. Instead, they communicate via Livewire events:
Component | Emits / Listens to | Purpose |
---|---|---|
Filters |
Emits: filter-updated , url-updated , meta-tags-ready-for-update
|
Informs others of filter changes and SEO updates |
Products |
Listens to: filter-updated , url-updated Emits: products-updated
|
Loads new products and informs others about the updated list |
ProductsSummarize |
Listens to: products-updated
|
Updates summary data based on the latest product data |
Why not use Livewire’s #[Url]
?
Livewire’s #[Url]
is excellent for simple reactive inputs (like search bars). However, for this project I needed:
- Full control over canonical and localized URLs
- Server-driven SEO meta data (title, canonical, robots)
- Advanced rules (like noindex for certain filter combinations)
- Structured filter state — not just flat query parameters
That’s why I implemented a dedicated UrlFiltersParser
and SEO data services.
It’s more work — but it ensures perfect SEO, clean architecture, and no SSR/hydration issues.
Testing
All critical logic — filter parsing, product loading, and SEO meta data generation — lives in dedicated services. These services are fully covered by unit tests (e.g., PHPUnit).
For Livewire components, I rely on Livewire’s testing utilities to verify event dispatching and UI state updates.
Summary
This architecture delivers:
- Loosely coupled, reusable components with a clear separation of UI and logic
- Thin and reactive components — no bloated logic or hard dependencies
- Dedicated, testable services that handle business logic (e.g., filter parsing, SEO generation)
- Event-driven communication through Livewire events, ensuring no direct dependencies
- Server-driven, indexable SEO data (title, canonical, robots) — safe for SSR and search engines
- No frontend frameworks — no JavaScript duplication or hydration issues
In Part 3, I’ll explain the role of the JS bridge and how it supports a smooth, dynamic UI experience.
Let’s Connect
If you’re looking for a senior developer to solve complex architecture challenges or lead critical parts of your product — let’s talk.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.