DEV Community

Mykola Vantukh
Mykola Vantukh

Posted on • Edited on

Laravel Livewire 3 Filter Architecture (Part 2)

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

Image description

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
)
Enter fullscreen mode Exit fullscreen mode

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.

View code on GitHub Gist


Products: Responsibilities

Image description

The Products component:

  • Listens for events like filter-updated, page-changed, and sort-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
)
Enter fullscreen mode Exit fullscreen mode
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

View code on GitHub Gist


ProductsSummarize: Responsibilities

Image description

The ProductsSummarize component is purely presentational:

  • It listens for products-loaded and products-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;
}
Enter fullscreen mode Exit fullscreen mode

View code on GitHub Gist


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.

Connect with me on LinkedIn

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.