Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

ctrlaltdelme's avatar

Sharing State between two+ of the same components in different areas of the page?

I have 3 components that are on a show page, and in a Modal that I'm triggering. I'm having trouble figuring out how to sync the state between the uses of a single one of these components. For example, I have this Watch Component that I want to share its state between the page and modal.

@if (Auth::check())
                    <form>
                        @csrf
                        <div class="flex items-center space-x-10 my-4">
                            <!-- TODO: Make one parent component that holds all 3? -->
                            {{-- Watched Date --}}
                            <livewire:watch-input :movie-id="$movie->id"/>

                            {{-- Rating Input --}}
                            <livewire:rating-input :movie="$movie"/>

                            {{-- Like Input --}}
                            <livewire:like-input :movie="$movie"/>

                            {{-- Watchlist --}}
                            {{-- Kebab case for the prop is okay --}}
                            <livewire:watchlist-input :movie-id="$movie->id"/>
                        </div>
                    </form>
                @else

The bottom is the modal form. Any tips are appreciated. I can't seem to understand what to do from reading the docs.

0 likes
12 replies
RemiM's avatar

You can use Livewire's Alpine store to share state globally across multiple instances.

Modify watch-input.blade.php

<div x-data="{ isWatched: $store.watchState['{{ $movieId }}'] ?? @json($isWatched) }">
    <div class="cursor-pointer" 
        wire:click="toggleWatch" 
        x-on:click="isWatched = !isWatched; $store.watchState['{{ $movieId }}'] = isWatched"
    >
        <x-icon-clock
            class="w-10 h-10 hover:text-primary-400 transition-colors"
            :class="isWatched ? 'fill-primary-500 stroke-current text-zinc-950' : 'fill-none text-primary-500'"
        />
    </div>
</div>

Add Alpine store in app.blade.php or layout.blade.php

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.store('watchState', {});
    });
</script>

This is the best for instant UI updates and lightweight approach.

Now, you could also do it with Livewire Events if Livewire components must stay in sync across re-renders.

Another solution would be to create a parent component if you want centralized state management.

1 like
ctrlaltdelme's avatar

@RemiM That's awesome thank you! I seem to be having a problem with this suggestion though. Not sure if I did something wrong.

<div x-data="{ isWatched: $store.watchState['{{ $movieId }}'] ?? @json($isWatched) }">
    <label class="block text-sm text-primary-400 mb-1">{{ $isWatched ? 'Watched' : 'Watch' }}</label>
    <div class="relative flex items-center group">
        <div class="cursor-pointer " wire:click="toggleWatch"
             x-on:click="isWatched = !isWatched; $store.watchState['{{ $movieId }}'] = isWatched">
            <x-icon-eye-outline
                class="w-10 h-10 hover:text-primary-400 transition-colors {{
                $isWatched ? 'fill-primary-500 stroke-current text-zinc-950' : 'fill-none text-primary-500'
                 }}"/>
        </div>
    </div>
</div>

And I put this script in my app.blade.php, but there's still a desync between the page and the modal

https://i.imgur.com/gQf1P52.png https://i.imgur.com/EoBBpjS.png

Do I need to do anything in the component?

RemiM's avatar

So I made couple changes here:

<div x-data="{ 
        isWatched: $store.watchState['{{ $movieId }}'] ?? @json($isWatched),
        toggleWatch() {
            this.isWatched = !this.isWatched; 
            $store.watchState['{{ $movieId }}'] = this.isWatched;
            @this.call('toggleWatch'); // Sync with Livewire
        }
    }">
    <label class="block text-sm text-primary-400 mb-1">{{ isWatched ? 'Watched' : 'Watch' }}</label>
    <div class="relative flex items-center group">
        <div class="cursor-pointer" x-on:click="toggleWatch">
            <x-icon-eye-outline
                class="w-10 h-10 hover:text-primary-400 transition-colors {{
                isWatched ? 'fill-primary-500 stroke-current text-zinc-950' : 'fill-none text-primary-500'
                }}"/>
        </div>
    </div>
</div>
  1. Removed wire:click: Since we're already managing the toggleWatch functionality with Alpine via x-on:click, the wire:click is redundant and unnecessary. Using both can cause issues because Livewire will try to trigger the same method independently from Alpine.

  2. Addition of the Alpine toggleWatch() method: this method toggles the isWatched state on the frontend using Alpine and then calls the toggleWatch() method in Livewire via @this.call('toggleWatch'). This ensures that when Alpine updates the state, Livewire's backend also gets updated.

Let me know if this solve the issue.

1 like
ctrlaltdelme's avatar

@RemiM It's still working on the show page where the component is used, but not in the Modal. Does it matter that the Modal is just a plain Blade Component and not a Livewire component? I feel like this should work given my understanding of it, but it still won't sync. The state looks the same as the screenshots from my last post.

It is triggering the Livewire method though and updating the DB so that's working. It just isn't communicating itself to itself in the modal

ctrlaltdelme's avatar

@remim any ideas? Would it make sense to just make the show page a full page Livewire component?

RemiM's avatar
  1. In the toggleWatch method, append a dispatch call at the end:
    $this->dispatch('watchToggled', $this->movieId, $this->isWatched);
  1. Add an x-init attribute to your parent div, alongside x-data, with the following:
x-init="
        Livewire.on('watchToggled', (movieId, newState) => {
            if (movieId == {{ $movieId }}) {
                isWatched = newState;
                $store.watchState[movieId] = newState;
            }
        });
    "

Now, just like you mentioned it, consider making it a Livewire component instead of a Blade component. That way, when isWatched updates in the database, Livewire will automatically refresh the modal state.

RemiM's avatar

The movieIdstarts in the Livewire component, then it's passed to the Blade view, where it's injected into the Alpine component. The Alpine component uses movieId to manage state in the store, and to synchronize with Livewire events.

ctrlaltdelme's avatar

@RemiM I had tried your suggestion and still found it wasn't sharing the state. So, I started over on my own trying to read through the Livewire docs to figure this out. I have something so far that will trigger the watch state (albeit a bit slower than yours I think) and it syncs it with the db, calling Livewire.

watch-input.blade.php (Livewire)

<div
    x-data="{
        isWatched: $wire.entangle('isWatched').defer}"
    x-on:watch-toggled.window="isWatched = $event.detail.isWatched"

>
    <label class="block text-sm text-primary-400 mb-1">{{ $isWatched ? 'Watched' : 'Watch' }}</label>
    <div class="relative flex items-center group">
        <div class="cursor-pointer" x-on:click="$wire.toggleWatch">
            <x-icon-eye-outline
                class="w-10 h-10 hover:text-primary-400 transition-colors {{
                $isWatched ? 'fill-primary-500 stroke-current text-zinc-950' : 'fill-none text-primary-500'
                }}"/>
        </div>
    </div>
</div>

WatchInput.php

show.blade.php (/movies/show) (Non-Livewire)

review-modal.blade.php (Non-Livewire)

I think I'm close. For some reason the store just wasn't working. I would check the DOM and nothing was being stored. The script wouldn't change at all.

RemiM's avatar

If it's the store that isn't working, maybe it's because you are assuming Livewire automatically puts data inside detail in your x-on attribute. I think to fix it, you can send an associate array, so that it exists in Alpine, and this way, it can properly extract isWatched from $event.detail.

You simply alter your dispatch method:

$this->dispatch('watch-toggled', ['movieId' => $this->movieId, 'isWatched' => $this->isWatched]);

maybe you can add a console.log as well to check if it's working:

x-on:watch-toggled.window="isWatched = $event.detail.isWatched; console.log($event.detail)"
ctrlaltdelme's avatar

@RemiM That sort of works. I can console.log the event.detail where I noticed it's an array with just one index, so detail[0] gives you the event Object. But the WatchInput inside the modal is still not syncing with the WatchInput on the show page. I'm stuck at what to do here.

EDIT: Nevermind! I had to create a Livewire Action to handle itself being toggled. I'm not sure if this is the best way to be doing this as it is a little slow to update sometimes (as was my initial problem). Is there a better way to toggle?

<div
    x-data="{
        isWatched: $wire.entangle('isWatched').defer}"
    x-on:watch-toggled="isWatched = $event.detail[0].isWatched"

>
    <label class="block text-sm text-primary-400 mb-1">{{ $isWatched ? 'Watched' : 'Watch' }}</label>
    <div class="relative flex items-center group">
        <div class="cursor-pointer" x-on:click="$wire.toggleWatch">
            <x-icon-eye-outline
                class="w-10 h-10 hover:text-primary-400 transition-colors {{
                $isWatched ? 'fill-primary-500 stroke-current text-zinc-950' : 'fill-none text-primary-500'
                }}"/>
        </div>
    </div>
</div>
RemiM's avatar

Well, I definitely think there's better options, especially with Livewire 3.

The template part could be even simplified:

<div>
    <label class="block text-sm text-primary-400 mb-1">{{ $isWatched ? 'Watched' : 'Watch' }}</label>
    <div class="relative flex items-center group">
        <div class="cursor-pointer" wire:click="toggleWatch">
            <x-icon-eye-outline
                class="w-10 h-10 hover:text-primary-400 transition-colors {{
                $isWatched ? 'fill-primary-500 stroke-current text-zinc-950' : 'fill-none text-primary-500'
                }}"/>
        </div>
    </div>
</div>

The class would have some small changes as well, especially Livewire's built-in broadcasting .to(*) method, which is much cleaner than trying to manually sync with Alpine in the end:

Please or to participate in this conversation.