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.