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

naykel's avatar

How to prevent Choices.js from resetting on Livewire updates?

Hi all,

I’m building a reusable multi-select component using Laravel Livewire 3, AlpineJS 3, and Choices.js.

Individually, everything works fine — but when I combine them, the Choices instance resets every time the Livewire property updates.

Most solutions I’ve found either suffer from the same issue or don’t work at all.

From what I can tell, this is due to how Livewire reactivity works. When a Livewire property changes, it re-renders the DOM, which in turn reinitialises Alpine and Choices — wiping out the UI state.

Has anyone solved this properly?

I’m looking for a solution that:

  • Keeps the Choices.js instance persistent across Livewire updates
  • Still syncs the selected values properly with Livewire using wire:model or $wire.set()

I’m sick of wrestling with this. Any working approach or example would be much appreciated.

@props([
    'for' => null,
    'options' => [],
    'placeholder' => 'Select an option',
])

@php
    $model = $attributes->whereStartsWith('wire:model')->first();

    if (!isset($model)) {
        throw new InvalidArgumentException('The wire:model attribute must be specified for the editor control.');
    }
@endphp

<select {{ $attributes }}
    x-data="choicesComponent($wire.entangle('{{ $model }}'))"
    x-ref="selectElement"
    multiple>
    @foreach ($options as $key => $value)
        <option value="{{ $key }}">{{ $value }}</option>
    @endforeach
</select>

@pushOnce('scripts')
    <script>
        document.addEventListener('alpine:init', () => {
            Alpine.data('choicesComponent', (selections) => ({
                selections,
                choices: null,
                init() {
                    this.choices = new Choices(this.$refs.selectElement, {
                        removeItemButton: true,
                        searchEnabled: true,
                        placeholderValue: '{{ $placeholder }}',
                    });

                    this.choices.setChoiceByValue(this.selections);

                    this.$refs.selectElement.addEventListener('change', () => {
                        const values = Array.from(this.$refs.selectElement.selectedOptions)
                            .map(option => option.value);
                        console.log('Selected values:', values);
                    });
                }
            }));
        });
    </script>
@endPushOnce

@pushOnce('scripts')
    <script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
@endPushOnce
0 likes
5 replies
LaryAI's avatar

You're correct: the issue is that Livewire re-renders the <select>, destroying and recreating the Choices.js instance, which resets the UI state. To solve this, you need to prevent Livewire from touching the DOM for the select element, and handle syncing yourself.

Here’s a robust approach:

1. Use wire:ignore on the <select>

This tells Livewire to leave the DOM alone for this element, so Choices.js can manage it.

2. Sync Choices.js selections with Livewire manually

Use Alpine’s $watch or event listeners to update Livewire when the selection changes, and also watch for changes from Livewire to update Choices.js.


Here’s a working example:


Key Points:

  • wire:ignore prevents Livewire from re-rendering the select.
  • Use Alpine’s $watch to update Choices.js when Livewire changes.
  • Use Choices.js events to update Livewire when the user changes the selection.
  • Use @entangle(...).defer for two-way binding with Alpine and Livewire.

This pattern is the most reliable way to integrate Choices.js (or any JS UI library) with Livewire.
If you need to update the options list dynamically, you’ll need to handle that separately (e.g., by destroying and re-initializing Choices.js when options change).

Let me know if you need a more advanced example!

valentin_vranic's avatar

What happens if you change

@pushOnce('scripts') to livewire concise @script and @endscript?

and the choices lib under

@assets and @endassets

valentin_vranic's avatar

@FrogMen I haven't got a notification about your response. Well, what come up on my mind was, have you tried to add wire:ignore on select? That would prevent to re-render the choicesjs on it. And still define everything in your init?

masokky's avatar

I've solved this issue manually parsing component.lastFreshHtml property in Livewire.hook, every time you change the choices livewire will return the generated HTML, we have to parse the options and assign them to ChoicesJs.
I am using Livewire 2 by the way, I don't know this code will work with Livewire 3 or not. I haven't tried it.

Here's my code

Livewire.hook('message.processed', (message, component) => {
            const officeField = $('#office');
            const parser = new DOMParser();
            const doc = parser.parseFromString(component.lastFreshHtml, 'text/html');
            const options = doc.querySelectorAll('#office option');

            const choices = Array.from(options)
                .map(opt => ({
                    value: opt.value,
                    label: opt.textContent.trim(),
                    selected: opt.hasAttribute('selected')
                }));

            officeField.clearStore();

            officeField.setChoices(choices, 'value', 'label', true);
        });

Please or to participate in this conversation.