Suppose you have multiple parent components using the same child view component, but each parent sends different dynamic parameters from the database.
For example:
Doctor Parent Component
↓
uses same child details card
Hospital Parent Component
↓
uses same child details card
Organizer Parent Component
↓
uses same child details card
Team Member Parent Component
↓
uses same child details card
The child UI is the same, but the data is different.
Real Use Case
Imagine you want to reuse one child card component for different modules:
Doctors
Hospitals
Organizers
Team Members
Countries
KYC Documents
Service Areas
Each parent has different database data, but the child component design remains the same.
Example child component:
Title
Subtitle
Status badge
Description
Primary button
Secondary button
Metadata
Instead of creating separate card components like:
doctor-card.blade.php
hospital-card.blade.php
organizer-card.blade.php
team-member-card.blade.php
You create one reusable component:
dynamic-info-card.blade.php
Then each parent sends dynamic data from the database.
Blog Topic Idea
How to Reuse One Child View Component Across Multiple Parent Components with Dynamic Database Parameters in Laravel Livewire and Alpine.js
What We Are Building
We will build one reusable child component:
<x-admin.dynamic-info-card />
This child component will be used by multiple parent Livewire components.
Each parent will send different database data like:
Doctor data
Hospital data
Organizer data
Team member data
The child component will not know which module is using it. It will only receive a clean dynamic parameter array.
Recommended Folder Structure
resources/
├── views/
│ ├── components/
│ │ └── admin/
│ │ └── dynamic-info-card.blade.php
│ │
│ └── livewire/
│ └── admin/
│ ├── doctors/
│ │ └── doctors-index.blade.php
│ │
│ ├── hospitals/
│ │ └── hospitals-index.blade.php
│ │
│ └── organizers/
│ └── organizers-index.blade.php
│
├── app/
│ └── Livewire/
│ └── Admin/
│ ├── Doctors/
│ │ └── DoctorsIndex.php
│ │
│ ├── Hospitals/
│ │ └── HospitalsIndex.php
│ │
│ └── Organizers/
│ └── OrganizersIndex.php
Main Concept
The parent component should prepare data in a common format.
The child component should only display that common format.
That means every parent should convert its database data into this structure:
[
'title' => '',
'subtitle' => '',
'description' => '',
'status' => '',
'badgeColor' => '',
'meta' => [],
'primaryAction' => '',
'secondaryAction' => '',
]
Now the child component does not care if the data came from doctors, hospitals, or organizers.
Step 1: Create the Reusable Child Component
Create this file:
resources/views/components/admin/dynamic-info-card.blade.php
Add this code:
@props([
'item' => [],
])
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm hover:shadow-md transition">
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="text-lg font-semibold text-gray-900">
{{ $item['title'] ?? 'No Title' }}
</h3>
<p class="mt-1 text-sm text-gray-500">
{{ $item['subtitle'] ?? 'No Subtitle' }}
</p>
</div>
@if(!empty($item['status']))
<span class="rounded-full px-3 py-1 text-xs font-medium {{ $item['badgeClass'] ?? 'bg-gray-100 text-gray-700' }}">
{{ $item['status'] }}
</span>
@endif
</div>
@if(!empty($item['description']))
<p class="mt-4 text-sm leading-6 text-gray-600">
{{ $item['description'] }}
</p>
@endif
@if(!empty($item['meta']) && is_array($item['meta']))
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
@foreach($item['meta'] as $label => $value)
<div class="rounded-lg bg-gray-50 p-3">
<p class="text-xs font-medium text-gray-500">
{{ $label }}
</p>
<p class="mt-1 text-sm font-semibold text-gray-900">
{{ $value }}
</p>
</div>
@endforeach
</div>
@endif
<div class="mt-5 flex flex-wrap gap-2">
@if(!empty($item['primaryAction']))
<a
href="{{ $item['primaryAction']['url'] ?? '#' }}"
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
{{ $item['primaryAction']['label'] ?? 'View' }}
</a>
@endif
@if(!empty($item['secondaryAction']))
<a
href="{{ $item['secondaryAction']['url'] ?? '#' }}"
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100"
>
{{ $item['secondaryAction']['label'] ?? 'Edit' }}
</a>
@endif
</div>
</div>
What This Child Component Does
This component accepts one dynamic parameter:
:item="$item"
The child component displays:
Title
Subtitle
Status
Description
Metadata
Buttons
But it does not know whether the data is doctor data, hospital data, or organizer data.
That is the main benefit.
Step 2: Doctor Parent Component
Create or open:
app/Livewire/Admin/Doctors/DoctorsIndex.php
Example code:
namespace App\Livewire\Admin\Doctors;
use App\Models\Doctor;
use Livewire\Component;
class DoctorsIndex extends Component
{
public function getDoctorsProperty()
{
return Doctor::query()
->latest()
->get()
->map(function ($doctor) {
return [
'title' => $doctor->name,
'subtitle' => $doctor->specialization ?? 'General Doctor',
'description' => $doctor->bio ?? 'No doctor description available.',
'status' => ucfirst($doctor->status ?? 'pending'),
'badgeClass' => $this->getStatusClass($doctor->status),
'meta' => [
'Email' => $doctor->email ?? 'N/A',
'Phone' => $doctor->phone ?? 'N/A',
'City' => $doctor->city ?? 'N/A',
'Experience' => ($doctor->experience ?? 0) . ' years',
],
'primaryAction' => [
'label' => 'View Doctor',
'url' => route('admin.doctors.show', $doctor->id),
],
'secondaryAction' => [
'label' => 'Edit Doctor',
'url' => route('admin.doctors.edit', $doctor->id),
],
];
});
}
private function getStatusClass($status): string
{
return match ($status) {
'approved' => 'bg-green-100 text-green-700',
'blocked' => 'bg-red-100 text-red-700',
'pending' => 'bg-yellow-100 text-yellow-700',
default => 'bg-gray-100 text-gray-700',
};
}
public function render()
{
return view('livewire.admin.doctors.doctors-index');
}
}
Now create the Blade view:
resources/views/livewire/admin/doctors/doctors-index.blade.php
<div class="p-6">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">
Doctors
</h1>
<p class="mt-1 text-sm text-gray-500">
Manage doctor profiles from one place.
</p>
</div>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3">
@foreach($this->doctors as $doctorCard)
<x-admin.dynamic-info-card :item="$doctorCard" />
@endforeach
</div>
</div>
Here, the parent sends doctor data to the same child component.
Step 3: Hospital Parent Component
Now use the same child component for hospitals.
Create or open:
app/Livewire/Admin/Hospitals/HospitalsIndex.php
namespace App\Livewire\Admin\Hospitals;
use App\Models\Hospital;
use Livewire\Component;
class HospitalsIndex extends Component
{
public function getHospitalsProperty()
{
return Hospital::query()
->latest()
->get()
->map(function ($hospital) {
return [
'title' => $hospital->name,
'subtitle' => $hospital->type ?? 'Multi-speciality Hospital',
'description' => $hospital->about ?? 'No hospital description available.',
'status' => ucfirst($hospital->status ?? 'pending'),
'badgeClass' => $this->getStatusClass($hospital->status),
'meta' => [
'Email' => $hospital->email ?? 'N/A',
'Phone' => $hospital->phone ?? 'N/A',
'City' => $hospital->city ?? 'N/A',
'Beds' => $hospital->beds_count ?? 'N/A',
],
'primaryAction' => [
'label' => 'View Hospital',
'url' => route('admin.hospitals.show', $hospital->id),
],
'secondaryAction' => [
'label' => 'Edit Hospital',
'url' => route('admin.hospitals.edit', $hospital->id),
],
];
});
}
private function getStatusClass($status): string
{
return match ($status) {
'approved' => 'bg-green-100 text-green-700',
'blocked' => 'bg-red-100 text-red-700',
'pending' => 'bg-yellow-100 text-yellow-700',
default => 'bg-gray-100 text-gray-700',
};
}
public function render()
{
return view('livewire.admin.hospitals.hospitals-index');
}
}
Now create the hospital parent Blade view:
resources/views/livewire/admin/hospitals/hospitals-index.blade.php
<div class="p-6">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">
Hospitals
</h1>
<p class="mt-1 text-sm text-gray-500">
Manage hospital profiles and verification details.
</p>
</div>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3">
@foreach($this->hospitals as $hospitalCard)
<x-admin.dynamic-info-card :item="$hospitalCard" />
@endforeach
</div>
</div>
Notice the important part:
<x-admin.dynamic-info-card :item="$hospitalCard" />
This is the same child component used in the doctor parent.
Only the database source and mapped fields are different.
Step 4: Organizer Parent Component
Now use the same child component for organizers.
Create or open:
app/Livewire/Admin/Organizers/OrganizersIndex.php
namespace App\Livewire\Admin\Organizers;
use App\Models\Organizer;
use Livewire\Component;
class OrganizersIndex extends Component
{
public function getOrganizersProperty()
{
return Organizer::query()
->latest()
->get()
->map(function ($organizer) {
return [
'title' => $organizer->business_name,
'subtitle' => $organizer->category ?? 'Travel Organizer',
'description' => $organizer->description ?? 'No organizer description available.',
'status' => ucfirst($organizer->verification_status ?? 'pending'),
'badgeClass' => $this->getStatusClass($organizer->verification_status),
'meta' => [
'Owner' => $organizer->owner_name ?? 'N/A',
'Country' => $organizer->country ?? 'N/A',
'Total Trips' => $organizer->trips_count ?? 0,
'Rating' => $organizer->rating ?? 'N/A',
],
'primaryAction' => [
'label' => 'View Organizer',
'url' => route('admin.organizers.show', $organizer->id),
],
'secondaryAction' => [
'label' => 'Edit Organizer',
'url' => route('admin.organizers.edit', $organizer->id),
],
];
});
}
private function getStatusClass($status): string
{
return match ($status) {
'verified' => 'bg-green-100 text-green-700',
'rejected' => 'bg-red-100 text-red-700',
'pending' => 'bg-yellow-100 text-yellow-700',
default => 'bg-gray-100 text-gray-700',
};
}
public function render()
{
return view('livewire.admin.organizers.organizers-index');
}
}
Now create the organizer parent Blade view:
resources/views/livewire/admin/organizers/organizers-index.blade.php
<div class="p-6">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">
Organizers
</h1>
<p class="mt-1 text-sm text-gray-500">
Manage travel organizers and their verification status.
</p>
</div>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3">
@foreach($this->organizers as $organizerCard)
<x-admin.dynamic-info-card :item="$organizerCard" />
@endforeach
</div>
</div>
Again, same child component:
<x-admin.dynamic-info-card :item="$organizerCard" />
Step 5: Add Routes
Example routes:
use App\Livewire\Admin\Doctors\DoctorsIndex;
use App\Livewire\Admin\Hospitals\HospitalsIndex;
use App\Livewire\Admin\Organizers\OrganizersIndex;
Route::prefix('admin')->name('admin.')->group(function () {
Route::get('/doctors', DoctorsIndex::class)->name('doctors.index');
Route::get('/hospitals', HospitalsIndex::class)->name('hospitals.index');
Route::get('/organizers', OrganizersIndex::class)->name('organizers.index');
});
For action URLs used inside the cards, you can define routes like:
Route::get('/doctors/{doctor}', [DoctorController::class, 'show'])->name('doctors.show');
Route::get('/doctors/{doctor}/edit', [DoctorController::class, 'edit'])->name('doctors.edit');
Route::get('/hospitals/{hospital}', [HospitalController::class, 'show'])->name('hospitals.show');
Route::get('/hospitals/{hospital}/edit', [HospitalController::class, 'edit'])->name('hospitals.edit');
Route::get('/organizers/{organizer}', [OrganizerController::class, 'show'])->name('organizers.show');
Route::get('/organizers/{organizer}/edit', [OrganizerController::class, 'edit'])->name('organizers.edit');
Step 6: Add Dynamic Button Type
Sometimes one parent may need different button text.
Doctor parent:
'primaryAction' => [
'label' => 'View Doctor',
'url' => route('admin.doctors.show', $doctor->id),
],
Hospital parent:
'primaryAction' => [
'label' => 'View Hospital',
'url' => route('admin.hospitals.show', $hospital->id),
],
Organizer parent:
'primaryAction' => [
'label' => 'View Organizer',
'url' => route('admin.organizers.show', $organizer->id),
],
The child component remains unchanged.
That is the power of dynamic parameters.
Step 7: Add Optional Fields Safely
Sometimes one parent may not have all fields.
For example, doctors may have experience, but organizers may not.
That is why the child component uses safe checks:
@if(!empty($item['description']))
<p>{{ $item['description'] }}</p>
@endif
And:
@if(!empty($item['meta']) && is_array($item['meta']))
@foreach($item['meta'] as $label => $value)
...
@endforeach
@endif
This prevents errors when some parent data is missing.
Step 8: Add Alpine.js for Dynamic View Mode
Now suppose you want the same parent page to support grid view and list view.
Create a small Alpine wrapper:
<div x-data="{ viewMode: 'grid' }" class="p-6">
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">
Doctors
</h1>
<p class="mt-1 text-sm text-gray-500">
Manage doctor profiles from one place.
</p>
</div>
<div class="flex gap-2">
<button
type="button"
@click="viewMode = 'grid'"
class="rounded-lg border px-3 py-2 text-sm"
:class="viewMode === 'grid' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'"
>
Grid
</button>
<button
type="button"
@click="viewMode = 'list'"
class="rounded-lg border px-3 py-2 text-sm"
:class="viewMode === 'list' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700'"
>
List
</button>
</div>
</div>
<div
:class="viewMode === 'grid'
? 'grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3'
: 'space-y-4'"
>
@foreach($this->doctors as $doctorCard)
<x-admin.dynamic-info-card :item="$doctorCard" />
@endforeach
</div>
</div>
Here Alpine controls only frontend UI behavior.
The child component still receives dynamic database parameters from the parent.
Step 9: Better Approach Using a Data Transformer Method
If your parent component becomes large, move the mapping logic into a separate method.
Example:
private function mapDoctorToCard($doctor): array
{
return [
'title' => $doctor->name,
'subtitle' => $doctor->specialization ?? 'General Doctor',
'description' => $doctor->bio ?? 'No doctor description available.',
'status' => ucfirst($doctor->status ?? 'pending'),
'badgeClass' => $this->getStatusClass($doctor->status),
'meta' => [
'Email' => $doctor->email ?? 'N/A',
'Phone' => $doctor->phone ?? 'N/A',
'City' => $doctor->city ?? 'N/A',
'Experience' => ($doctor->experience ?? 0) . ' years',
],
'primaryAction' => [
'label' => 'View Doctor',
'url' => route('admin.doctors.show', $doctor->id),
],
'secondaryAction' => [
'label' => 'Edit Doctor',
'url' => route('admin.doctors.edit', $doctor->id),
],
];
}
Then use it like this:
public function getDoctorsProperty()
{
return Doctor::query()
->latest()
->get()
->map(fn ($doctor) => $this->mapDoctorToCard($doctor));
}
This keeps the code clean.
Step 10: Create a Reusable PHP Trait for Common Status Classes
If many parents use status badges, create one trait.
Create:
app/Support/StatusBadgeClass.php
namespace App\Support;
trait StatusBadgeClass
{
public function statusClass(?string $status): string
{
return match ($status) {
'approved', 'verified', 'active' => 'bg-green-100 text-green-700',
'blocked', 'rejected', 'inactive' => 'bg-red-100 text-red-700',
'pending', 'draft' => 'bg-yellow-100 text-yellow-700',
default => 'bg-gray-100 text-gray-700',
};
}
}
Use it in parent Livewire components:
use App\Support\StatusBadgeClass;
class DoctorsIndex extends Component
{
use StatusBadgeClass;
public function getDoctorsProperty()
{
return Doctor::query()
->latest()
->get()
->map(function ($doctor) {
return [
'title' => $doctor->name,
'subtitle' => $doctor->specialization ?? 'General Doctor',
'description' => $doctor->bio ?? 'No description available.',
'status' => ucfirst($doctor->status ?? 'pending'),
'badgeClass' => $this->statusClass($doctor->status),
'meta' => [
'Email' => $doctor->email ?? 'N/A',
'Phone' => $doctor->phone ?? 'N/A',
],
'primaryAction' => [
'label' => 'View Doctor',
'url' => route('admin.doctors.show', $doctor->id),
],
];
});
}
}
Step 11: Add Reusable Card with Extra Slot Support
Sometimes one parent needs extra custom content inside the card.
For that, update the child component:
@props([
'item' => [],
])
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm hover:shadow-md transition">
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="text-lg font-semibold text-gray-900">
{{ $item['title'] ?? 'No Title' }}
</h3>
<p class="mt-1 text-sm text-gray-500">
{{ $item['subtitle'] ?? 'No Subtitle' }}
</p>
</div>
@if(!empty($item['status']))
<span class="rounded-full px-3 py-1 text-xs font-medium {{ $item['badgeClass'] ?? 'bg-gray-100 text-gray-700' }}">
{{ $item['status'] }}
</span>
@endif
</div>
@if(!empty($item['description']))
<p class="mt-4 text-sm leading-6 text-gray-600">
{{ $item['description'] }}
</p>
@endif
@if(!empty($item['meta']) && is_array($item['meta']))
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
@foreach($item['meta'] as $label => $value)
<div class="rounded-lg bg-gray-50 p-3">
<p class="text-xs font-medium text-gray-500">
{{ $label }}
</p>
<p class="mt-1 text-sm font-semibold text-gray-900">
{{ $value }}
</p>
</div>
@endforeach
</div>
@endif
@if(trim($slot) !== '')
<div class="mt-4 border-t border-gray-100 pt-4">
{{ $slot }}
</div>
@endif
<div class="mt-5 flex flex-wrap gap-2">
@if(!empty($item['primaryAction']))
<a
href="{{ $item['primaryAction']['url'] ?? '#' }}"
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
{{ $item['primaryAction']['label'] ?? 'View' }}
</a>
@endif
</div>
</div>
Now one parent can add extra content:
<x-admin.dynamic-info-card :item="$doctorCard">
<div class="text-sm text-gray-600">
Available for online consultation today.
</div>
</x-admin.dynamic-info-card>
Another parent can use the same component without a slot:
Best Practice
Use this pattern when:
Multiple parent components need the same UI layout.
Each parent has different database models.
The child component should remain reusable.
The parent can normalize data into a common array.
You want to avoid duplicate Blade markup.
Avoid this pattern when:
Each parent needs completely different UI.
The child component needs too many conditions.
The data structure is too different.
The component becomes harder to understand than separate components.
Final Flow
Database
↓
Parent Livewire Component
↓
Maps model data into common card array
↓
Parent Blade loops over mapped data
↓
Reusable child component receives :item
↓
Child component renders same UI with dynamic values
Final Conclusion
When multiple parent components share the same child view but send different dynamic parameters from the database, the cleanest solution is to create a common data contract.
The parent should prepare the data.
The child should only display the data.
This gives you:
Less duplicate code
Cleaner Blade files
Reusable UI
Better maintainability
Easy design updates
Consistent admin interface
Top comments (0)