Livewire crashes on render

What seems to be the problem:

I am making a simple component to manage tags. There’s an input box - when you type into it, it checks to see if there are any matching tags and generates a dropdown list. You can then select from that list or hit an Add button. The selected tags are then displayed about the input box, each with a button to delete the selection fo that tag. This all works fine for the first tag. As soon as you as you start to type in the input box for the next tag, Livewire crashes with an error referring to the rendering of the selected tag array - Trying to get property ‘name’ of non-object. Nothing in the render() method refers to that array, and it displayed correctly in the previous cycle.

The $selected_tags array is an array of an Eloquent model (super simple - basically only has a name property plus an id). Adding a new tag or picking a found existing tag works correctly - it shows up above the input box. Type one character into the input box and it crashes.

I’m pretty new at Livewire - sorry for all the code. All help most welcome. Thanks!!

Steps to Reproduce:

see above

Are you using the latest version of Livewire:

yes

Do you have any screenshots or code examples:

Here’s the class file:

class TagInput extends Component
{
    public $tag_input; // the search/enter input box
    public $found_tags; // tags matching search
    public $selected_tags; // collection of Tag objects
    public $status;

    public function mount()
    {
        $this->tag_input = '';
        $this->selected_tags = [];
        $this->found_tags = [];
        $this->status = 'nothing';
    }

    public function render()
    {
        if (strlen($this->tag_input) > 2) {
            $found = Tag::where('name', "like", $this->tag_input . "%")->get();

            if ($found) {
                //$this->status = "found";
                $this->found_tags = $found;
            } else {
                //$this->status = "not found";
            }
        } else {
            $this->found_tags = [];
        }

        return view('livewire.tag-input');
    }

    public function addExistingTag($found_id)
    {
        if (!in_array($this->found_tags[$found_id]->id, $this->selected_tags)) {
            $this->updateTags($this->found_tags[$found_id]);
        }

    }

    public function addNewTag()
    {
        $new_tag = Tag::create([
           'name' => $this->tag_input,
        ]);

        $this->updateTags($new_tag);
    }

    public function removeTag($tag_id)
    {
        unset($this->selected_tags[$tag_id]);
        $this->selected_tags = array_values($this->selected_tags);

    }

    private function updateTags(Tag $tag)
    {
        $this->selected_tags[$tag->id] = $tag;

        $this->tag_input = "";
        $this->found_tags = [];
    }
}

Here’s the blade file (sorry for the crappy Tailwind - still learning):

<div class="mt-3 border-2 text-red-500">{{ $status }}</div>
    <div class="flex">
        @foreach($selected_tags as $key => $tag)
            <div class="flex border-2 border-blue-300 bg-blue-300">
                <span class="">{{ $tag->name }}</span>
                <button wire:click="removeTag({{ $key }})" class="bg-red-800 text-red-100 m-2">X</button>
            </div>
        @endforeach
    </div>
    <div class="flex">
        <input type="text" wire:model="tag_input" class="m-3">
        <button wire:click="addNewTag">
            +
        </button>
    </div>


    <div x-data="{ open: true }" class="relative inline-block text-left">
        <div x-show="open" >
            <div class="rounded-md bg-white shadow-xs">
                @foreach($found_tags as $key => $tag)
                    <div class="py-1">
                        <a href="#"
                           wire:click="addExistingTag({{ $key }})"
                           
                        >
                            {{ $tag->name }}
                        </a>
                    </div>
                    <div class="border-t border-gray-100"></div>
                @endforeach
            </div>
        </div>
    </div>
2 Likes

it is empty and the foreach need one or more values

the @if is the solution

you need put values for foreach in the mount()

Here

@forelse is cleaner.

@forelse ($users as $user)
    <li>{{ $user->name }}</li>
@empty
    <p>No users</p>
@endforelse

@xxdalexx @hortigado Thank you for your responses.

I’m afraid that’s not the problem. The template renders correctly on the first pass when the $selected_tags array is empty. It’s only after the first element is added that the problem comes up. When that element is added, the page does render correctly - it blows up as soon as a character is typed into the input box. Since that input box is part of a wire:model, my assumption would be that this triggers the render() function, and for some reason at that point the values in the $selected_tags array are lost. Note that the key must still be there, as it does not consider the array to be empty. (I did switch my code to the @forelse syntax and got the same result - and it does look better :slight_smile: ).

As an experiment, I changed the @foreach($selected_tags as $key => $tag) loop so that it did not show any properties of the $tag. This worked correctly, so clearly the problem is that somehow the array values get nulled out.

In the Livewire docs (under Properties), it says that a property must be Properties can ONLY be either a JavaScript-friendly data types (string, int, array, boolean), OR an eloquent model (or collection of models).

Does having an array of Eloquent models violate this rule? I tried to make it into a Laravel Collection, but it got ugly.

Thanks again for your help.

1 Like

OK - I think I understand the behavior, which might prove useful to others.

After the first render, the object in the $selected_tags array became an array. I’m guessing this has something to do with Livewire internally, but it happens consistently: when an object is added to the array, it is an object. After it passes through the render method, it becomes an array (and obviously object oriented methods fail on it).

For my case, I was able to replace the entire object with one of it’s properties (that I use to display it) and it works correctly (and Livewire makes it all super cool :slight_smile:).

Thanks for your help.

Great , its a bit hard help you without watch error

I appreciate that - there were a lot of moving pieces here. Your reply put me on the right path - thank you!!

Yeah, it does funky things when objects and arrays start getting nested in and out. I ran into the same problem of objects getting converted to arrays, and also getting the corrupt data errors (the tamper protection) when sending it to child components. If you run into a trickier situation, just do the json encode/decode trick to convert the whole structure to an array.

I had the same issue where I was passing a regular collection after doing something like

Article::all()->map(function ($article) {
    return [
        'title' => $article->title,
        ...
});

I had to call toArray() to make sure this was converted to a proper JavaScript-friendly data types.

The very confusing thing for me was that it worked locally but not on production. I still don’t know why it worked locally tho :man_shrugging:

Anyway, thanks a lot for sharing you knowledge here :heart: