Debug School

rakesh kumar
rakesh kumar

Posted on • Edited on

How to Reuse One Child Modal Component Across Multiple Parent Components Using Alpine.js Store

Why This Topic Is Useful
Main Problem Developers Face
Reuse component using alpine js store
What Is a Child Component?
What Is a Parent Component?
What Is Alpine.store?
Why Use Alpine.store for Shared Modal?
coding example to reuse modal using alpine js store

Why This Topic Is Useful

Each parent needs the same popup/modal.

Instead of creating separate modals again and again, you create one child modal component and control it globally using Alpine.store().

This is very practical for Laravel Blade projects because many pages need common popups like:

Use Case Example
Delete confirmation “Are you sure you want to delete this doctor?”
View details modal Show doctor, hospital, booking, or user details
Edit popup Open same edit modal with different data
Image preview Open image preview from different cards
Booking action Confirm, cancel, approve, reject booking
Alert modal Show success/error message from different sections

Main Problem Developers Face

Without a shared Alpine store, developers often write duplicate modal code inside every parent component.

Example problem:

<!-- Parent 1 modal -->
<div x-data="{ open: false }">
  ...
</div>

<!-- Parent 2 modal -->
<div x-data="{ open: false }">
  ...
</div>

<!-- Parent 3 modal -->
<div x-data="{ open: false }">
  ...
</div>
Enter fullscreen mode Exit fullscreen mode

This becomes hard to maintain.

If you change modal design, you must update many places.

Better Solution

Main Problem Developers Face

Use one global store:

<script>
document.addEventListener('alpine:init', () => {
    Alpine.store('modal', {
        open: false,
        title: '',
        message: '',
        data: null,

        show(title, message, data = null) {
            this.title = title;
            this.message = message;
            this.data = data;
            this.open = true;
        },

        close() {
            this.open = false;
            this.title = '';
            this.message = '';
            this.data = null;
        }
    });
});
</script>
Enter fullscreen mode Exit fullscreen mode

Now any parent can open the same modal.

Example Parent Component 1

<button
    @click="$store.modal.show(
        'Delete Doctor',
        'Are you sure you want to delete this doctor?',
        { id: 101, type: 'doctor' }
    )"
    class="px-4 py-2 bg-red-600 text-white rounded-lg">
    Delete Doctor
</button>
Enter fullscreen mode Exit fullscreen mode

Example Parent Component 2

<button
    @click="$store.modal.show(
        'Cancel Booking',
        'Do you want to cancel this booking?',
        { id: 55, type: 'booking' }
    )"
    class="px-4 py-2 bg-yellow-600 text-white rounded-lg">
    Cancel Booking
</button>
Single Reusable Child Modal
<div
    x-data
    x-show="$store.modal.open"
    class="fixed inset-0 flex items-center justify-center bg-black/50 z-50"
    x-cloak>

    <div class="bg-white rounded-xl shadow-lg w-full max-w-md p-6">
        <h2 class="text-xl font-bold mb-3" x-text="$store.modal.title"></h2>

        <p class="text-gray-600 mb-5" x-text="$store.modal.message"></p>

        <div class="flex justify-end gap-3">
            <button
                @click="$store.modal.close()"
                class="px-4 py-2 border rounded-lg">
                Cancel
            </button>

            <button
                @click="console.log($store.modal.data)"
                class="px-4 py-2 bg-blue-600 text-white rounded-lg">
                Confirm
            </button>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Explain that in Laravel Blade or normal HTML pages, developers often repeat modal code many times. Alpine.store helps manage shared frontend state globally.

What Is a Child Component?

A child component is a reusable UI block, like:

Modal
Popup
Alert box
Confirm dialog
Image preview
Toast notification
Enter fullscreen mode Exit fullscreen mode

What Is a Parent Component?

A parent component is any section that triggers the child component.

Example:

Doctor card opens modal
Booking row opens modal
User table opens modal
Hospital profile opens modal
Enter fullscreen mode Exit fullscreen mode

What Is Alpine.store?**

Alpine.store() is used to create global state in Alpine.js. This means multiple components can read and update the same data.

5.

Why Use Alpine.store for Shared Modal?

Because it gives:

Advantage Benefit
One modal code No duplicate popup HTML
Global access Any button can open modal
Cleaner Blade files Less repeated frontend code
Easy maintenance Update design in one place
Dynamic data support Send title, message, ID, type, status
Better structure Parent handles trigger, child handles UI

Real Laravel Use Case

Example:

@foreach($doctors as $doctor)
    <button
        @click="$store.modal.show(
            'View Doctor',
            'Doctor: {{ $doctor->name }}',
            { id: '{{ $doctor->id }}', type: 'doctor' }
        )">
        View
    </button>
@endforeach
Enter fullscreen mode Exit fullscreen mode

One modal can display data from many doctors.

Best Practices

Use Alpine.store when the same component is needed by many sections. Keep modal UI separate. Pass only required data like ID, title, message, type, and action. Avoid putting heavy business logic directly inside Alpine. For Laravel actions like delete/update, call backend routes or APIs safely.

Parent component imports JS
Child modal component stays separate
Global state is managed through Alpine.store()
Same child component can be reused from multiple parent components
Enter fullscreen mode Exit fullscreen mode

coding example to reuse modal using alpine js store

Goal

You want this flow:

Parent Component 1
    ↓
opens same child modal

Parent Component 2
    ↓
opens same child modal

Parent Component 3
    ↓
opens same child modal

Reusable Child Modal Component
    ↓
controlled by Alpine.store()
Enter fullscreen mode Exit fullscreen mode

This is best when you have common UI like:

Delete confirmation modal
View details modal
Status update modal
Image preview modal
Booking cancel modal
Doctor approve/reject modal
Enter fullscreen mode Exit fullscreen mode

Recommended Folder Structure

resources/
├── views/
│   ├── layouts/
│   │   └── app.blade.php
│   │
│   ├── pages/
│   │   └── doctors/
│   │       └── index.blade.php
│   │
│   └── components/
│       └── modals/
│           └── reusable-action-modal.blade.php
│
├── js/
│   ├── app.js
│   │
│   ├── stores/
│   │   └── modal-store.js
│   │
│   └── components/
│       └── doctor-actions.js
Enter fullscreen mode Exit fullscreen mode

Install Required Frontend Packages

If Tailwind and Alpine are already installed, skip this.

npm install alpinejs
npm install -D tailwindcss postcss autoprefixer
Enter fullscreen mode Exit fullscreen mode

Optional component library:

npm install flowbite

or

npm install daisyui
Enter fullscreen mode Exit fullscreen mode

Create Alpine Store File

Create this file:

resources/js/stores/modal-store.js
export default function registerModalStore(Alpine) {
    Alpine.store('actionModal', {
        isOpen: false,
        title: '',
        message: '',
        confirmText: 'Confirm',
        cancelText: 'Cancel',
        actionType: null,
        payload: null,

        open(config = {}) {
            this.title = config.title || 'Confirm Action';
            this.message = config.message || 'Are you sure you want to continue?';
            this.confirmText = config.confirmText || 'Confirm';
            this.cancelText = config.cancelText || 'Cancel';
            this.actionType = config.actionType || null;
            this.payload = config.payload || null;
            this.isOpen = true;
        },

        close() {
            this.isOpen = false;
            this.title = '';
            this.message = '';
            this.confirmText = 'Confirm';
            this.cancelText = 'Cancel';
            this.actionType = null;
            this.payload = null;
        },

        confirm() {
            window.dispatchEvent(
                new CustomEvent('action-modal:confirmed', {
                    detail: {
                        actionType: this.actionType,
                        payload: this.payload,
                    },
                })
            );

            this.close();
        },
    });
}
Enter fullscreen mode Exit fullscreen mode

What this file does

This creates a global modal store named:

$store.actionModal
Enter fullscreen mode Exit fullscreen mode

Now any Blade file, parent component, table row, card, or button can open the same modal.

  1. Register Store in app.js

Open:

resources/js/app.js

Add this:

import Alpine from 'alpinejs';
import registerModalStore from './stores/modal-store';
import './components/doctor-actions';

window.Alpine = Alpine;

document.addEventListener('alpine:init', () => {
    registerModalStore(Alpine);
});

Alpine.start();
Enter fullscreen mode Exit fullscreen mode

Why this is important

This is the main entry file. It imports:

modal-store.js
doctor-actions.js

So your parent logic and child modal store stay clean and separate.

  1. Create Parent Action JS File

Create:

resources/js/components/doctor-actions.js
Enter fullscreen mode Exit fullscreen mode
window.addEventListener('action-modal:confirmed', function (event) {
    const { actionType, payload } = event.detail;

    if (!actionType || !payload) {
        return;
    }

    if (actionType === 'delete-doctor') {
        deleteDoctor(payload);
    }

    if (actionType === 'block-doctor') {
        blockDoctor(payload);
    }

    if (actionType === 'approve-doctor') {
        approveDoctor(payload);
    }
});

function deleteDoctor(payload) {
    const form = document.getElementById(`delete-doctor-form-${payload.id}`);

    if (form) {
        form.submit();
    }
}

function blockDoctor(payload) {
    const form = document.getElementById(`block-doctor-form-${payload.id}`);

    if (form) {
        form.submit();
    }
}

function approveDoctor(payload) {
    const form = document.getElementById(`approve-doctor-form-${payload.id}`);

    if (form) {
        form.submit();
    }
}
Enter fullscreen mode Exit fullscreen mode

What this file does

This listens when the child modal confirms an action.

Example:

User clicks Delete Doctor
↓
Parent opens modal
↓
User clicks Confirm
↓
Modal dispatches event
↓
doctor-actions.js catches event
↓
Correct form submits
Enter fullscreen mode Exit fullscreen mode

This keeps business action logic separate from modal UI.

  1. Create Reusable Child Modal Component

Create:

resources/views/components/modals/reusable-action-modal.blade.php
Enter fullscreen mode Exit fullscreen mode
<div
    x-data
    x-show="$store.actionModal.isOpen"
    x-cloak
    class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4"
    @keydown.escape.window="$store.actionModal.close()"
>
    <div
        class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl"
        @click.outside="$store.actionModal.close()"
    >
        <div class="mb-4">
            <h2
                class="text-xl font-semibold text-gray-900"
                x-text="$store.actionModal.title"
            ></h2>

            <p
                class="mt-2 text-sm text-gray-600"
                x-text="$store.actionModal.message"
            ></p>
        </div>

        <div class="flex justify-end gap-3">
            <button
                type="button"
                class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100"
                @click="$store.actionModal.close()"
                x-text="$store.actionModal.cancelText"
            ></button>

            <button
                type="button"
                class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
                @click="$store.actionModal.confirm()"
                x-text="$store.actionModal.confirmText"
            ></button>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Important

Add this CSS once to hide Alpine content before load:


[x-cloak] {
    display: none !important;
}
Enter fullscreen mode Exit fullscreen mode

Usually add it in:

resources/css/app.css

  1. Include Child Modal in Main Layout

Open:

resources/views/layouts/app.blade.php

Example:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Admin Panel</title>

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

<body class="bg-gray-100">

    <main>
        @yield('content')
    </main>

    {{-- Global reusable modal --}}
    <x-modals.reusable-action-modal />

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Why include it in layout?

Because this modal becomes globally available on every page.

So any parent component can open it.

  1. Parent Component Example: Doctor List Page

Create or open:

resources/views/pages/doctors/index.blade.php
Enter fullscreen mode Exit fullscreen mode
@extends('layouts.app')

@section('content')
<div class="mx-auto max-w-6xl p-6">
    <h1 class="mb-6 text-2xl font-bold text-gray-900">
        Doctors List
    </h1>

    <div class="overflow-hidden rounded-xl bg-white shadow">
        <table class="w-full border-collapse">
            <thead class="bg-gray-50">
                <tr>
                    <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700">
                        Name
                    </th>
                    <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700">
                        Email
                    </th>
                    <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700">
                        Action
                    </th>
                </tr>
            </thead>

            <tbody>
                @foreach($doctors as $doctor)
                    <tr class="border-t">
                        <td class="px-4 py-3 text-sm text-gray-800">
                            {{ $doctor->name }}
                        </td>

                        <td class="px-4 py-3 text-sm text-gray-600">
                            {{ $doctor->email }}
                        </td>

                        <td class="px-4 py-3 text-right">
                            <button
                                type="button"
                                class="rounded-lg bg-red-600 px-4 py-2 text-sm text-white hover:bg-red-700"
                                @click="$store.actionModal.open({
                                    title: 'Delete Doctor',
                                    message: 'Are you sure you want to delete {{ $doctor->name }}?',
                                    confirmText: 'Yes, Delete',
                                    cancelText: 'No, Cancel',
                                    actionType: 'delete-doctor',
                                    payload: {
                                        id: {{ $doctor->id }}
                                    }
                                })"
                            >
                                Delete
                            </button>

                            <form
                                id="delete-doctor-form-{{ $doctor->id }}"
                                action="{{ route('doctors.destroy', $doctor->id) }}"
                                method="POST"
                                class="hidden"
                            >
                                @csrf
                                @method('DELETE')
                            </form>
                        </td>
                    </tr>
                @endforeach
            </tbody>
        </table>
    </div>
</div>
@endsection
Enter fullscreen mode Exit fullscreen mode
  1. Another Parent Component: Block Doctor Button

Same child modal can be used here also.

<button
    type="button"
    class="rounded-lg bg-yellow-500 px-4 py-2 text-sm text-white hover:bg-yellow-600"
    @click="$store.actionModal.open({
        title: 'Block Doctor',
        message: 'Are you sure you want to block this doctor account?',
        confirmText: 'Yes, Block',
        cancelText: 'Cancel',
        actionType: 'block-doctor',
        payload: {
            id: {{ $doctor->id }}
        }
    })"
>
    Block
</button>

<form
    id="block-doctor-form-{{ $doctor->id }}"
    action="{{ route('doctors.block', $doctor->id) }}"
    method="POST"
    class="hidden"
>
    @csrf
    @method('PATCH')
</form>
Enter fullscreen mode Exit fullscreen mode
  1. Another Parent Component: Approve Doctor Button
<button
    type="button"
    class="rounded-lg bg-green-600 px-4 py-2 text-sm text-white hover:bg-green-700"
    @click="$store.actionModal.open({
        title: 'Approve Doctor',
        message: 'Do you want to approve this doctor profile?',
        confirmText: 'Yes, Approve',
        cancelText: 'Cancel',
        actionType: 'approve-doctor',
        payload: {
            id: {{ $doctor->id }}
        }
    })"
>
    Approve
</button>

<form
    id="approve-doctor-form-{{ $doctor->id }}"
    action="{{ route('doctors.approve', $doctor->id) }}"
    method="POST"
    class="hidden"
>
    @csrf
    @method('PATCH')
</form>
Enter fullscreen mode Exit fullscreen mode
  1. Example Routes
use App\Http\Controllers\DoctorController;
Enter fullscreen mode Exit fullscreen mode
Route::delete('/doctors/{doctor}', [DoctorController::class, 'destroy'])
    ->name('doctors.destroy');

Route::patch('/doctors/{doctor}/block', [DoctorController::class, 'block'])
    ->name('doctors.block');

Route::patch('/doctors/{doctor}/approve', [DoctorController::class, 'approve'])
    ->name('doctors.approve');
Enter fullscreen mode Exit fullscreen mode
  1. Example Controller
namespace App\Http\Controllers;

use App\Models\Doctor;
use Illuminate\Http\RedirectResponse;

class DoctorController extends Controller
{
    public function destroy(Doctor $doctor): RedirectResponse
    {
        $doctor->delete();

        return back()->with('success', 'Doctor deleted successfully.');
    }

    public function block(Doctor $doctor): RedirectResponse
    {
        $doctor->update([
            'status' => 'blocked',
        ]);

        return back()->with('success', 'Doctor blocked successfully.');
    }

    public function approve(Doctor $doctor): RedirectResponse
    {
        $doctor->update([
            'status' => 'approved',
        ]);

        return back()->with('success', 'Doctor approved successfully.');
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Build Frontend Assets

Run:

npm run dev
Enter fullscreen mode Exit fullscreen mode

For production:

npm run build
Enter fullscreen mode Exit fullscreen mode

Final Practical Flow

resources/js/stores/modal-store.js
    ↓
creates global Alpine modal store

resources/views/components/modals/reusable-action-modal.blade.php
    ↓
child modal reads store data

resources/views/pages/doctors/index.blade.php
    ↓
parent buttons open modal with different data

resources/js/components/doctor-actions.js
    ↓
handles confirm event and submits correct form
Enter fullscreen mode Exit fullscreen mode

Best Practice

Use this approach when the child component is common and reusable, like:

Confirm modal
Alert modal
Image preview modal
Status update modal
Delete popup
Approval popup
Enter fullscreen mode Exit fullscreen mode

reuse

Top comments (0)