livewire: Model variable resets or doesn't carry the state after every request

Description

A Model variable resets at every request / attempt to add attributes to it.

Exact steps to reproduce

I have a User model stored in a public variable $user. I set the initial value from the mount method.

On a different function, that is called using $listeners, I would add some properties, such as a relationship, a database value, etc. No matter what I add (or even do nothing to it), the variable clears the previous state and re-initialise again. This is an issue only with variables that are used for Models.

Stripped-down, copy-pastable code snippets

<?php

class Create extends Component
{
    public User $user;

    public $userArray = [];

    protected $listeners = [
        'step1:completed' => 'setStep1Data',
        'step2:completed' => 'setStep2Data',
        'step3:completed' => 'setStep3Data',
        'step4:completed' => 'setStep4Data',
    ];

    public function aNormalFunctionCalledDirectlyFromTheComponentPage()
    {
        // Whatever you do here, the variable $this->user re-initialise itself with an empty object.
    }

    public function setStep1Data( $contact )
    {
        // This works, it sets the contact relation...
        $this->user->setRelation('contact', new Contact($contact));
        
        // The state of userArray is not getting reset. It works fine, as intended.
        // For a moment, the user variable state is working fine
        $this->userArray = array_merge($this->user->toArray(), $this->userArray);
        
        // BUT once the function terminates, it will have ONLY the contact relation set, 
        // the name property set by the mount() method is gone.
    }

    public function setStep2Data( $email )
    {
        // This works, it sets the email property
        $this->user->email = $email;
    
        // The state of userArray is not getting reset. It works fine, as intended.
        // For a moment, the user variable state is working fine
        $this->userArray = array_merge($this->user->toArray(), $this->userArray);
    
        // BUT once the function terminates, it will have ONLY the email property set, 
        // the contact relation set by the setStep1Data() method is gone.
    }

    public function setStep3Data( $data )
    {
        // ...
    }

    public function setStep4Data( $data )
    {
        // ...
    }

    public function mount()
    {
        $this->user = new User();
        $this->user->name = 'John Doe';

        $this->userArray = array_merge($this->user->toArray(), $this->userArray);
    }

    public function render()
    {
        return view('livewire.users.create');
    }
}

Context

  • Livewire version: [2.2.7]
  • Laravel version: [8.6.0]
  • Browser: [Chrome, Safari]

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 1
  • Comments: 19 (7 by maintainers)

Most upvoted comments

Hi, I’m also experiencing this bug(?). Is there an upcoming fix for this?

From reading this issue I understand that you need to store the “model state” as an array, since a model won’t work.

This is however quite the workaround when you want to create multistep forms for example.

Now, about property manipulation. If you’re working with a User model, then you don’t need to have a separate action for “setting its attributes”, Livewire can directly bind to them and validate accordingly. Here’s a Playground using Livewire’s model-binding.

I am aware of this, the reason why I’ve wrote a specific function to change the property value is to show how Livewire resets the $user model before each request. I believe this is an unintended behaviour, but I might be wrong and this is the way it is.

If you would also like to keep your component as small as possible, then you can opt to use PHP’s Traits and separate each step into a Trait, and just use it in your Livewire component. (But keep in mind that function and property names may collide, so make sure to name them accordingly on each trait).

Thanks for the advise, I might struggle a bit as I love working with Objects rather than arrays, especially when there is a relationship involved.

My scope was to build the Model object step by step (don’t look at the examples, it’s more complex than that), such as adding properties and relationships of any type (hasOne, hasMany, etc). and at the end, create it in one go.

Thank’s so much for your help though, you’ve been amazing trying to help me. Hopefully this thread is going to be helpful for others too.

Most likely the last test for today:

I’ve done another test and it seems that livewire loose the object state before each request,

https://laravelplayground.com/#/snippets/44dca2c7-1bbb-4314-802d-269bdd648acc

Clicking “Add New User” will push a new User object into the array, but if you click it again, it coverts the previous entries in arrays, and only the last entry is an object.

Now I got what you’re trying to achieve. Arrays don’t save information on model data. If you’re planning to keep information for multiple models, then your only option at the moment is to save that information as a normal array, and later on process it as a model for DB persistence.

Checkout this example playground.

As a temporary solution (I might leave it for good though), I’m saving the user model on the session driver - and it works perfectly.

protected $queryString = [ 'identifier' ];

public function mount()
{
    if ( !$this->identifier ) {
        $this->identifier = $this->generate_numeric_code();
    }
    
    $this->user = new User();
    
    session()->put("user:{$this->identifier}", $this->user);
}

public function setStep1Data( $contact )
{
    $this->user = session()->get("user:{$this->identifier}");

    $this->user->setRelation('contact', new Contact($contact));

    $this->setStepData(0, $contact);

    session()->put("user:{$this->identifier}", $this->user);
}

Looks like you’re missing your protected $rules attribute, defining the $user validation properties. It is specified in Livewire’s Documentation that you need to provide a $rules property for your component, otherwise, errors may arise.

In your example, you want to keep the name and email of the user, so your $rules property should be specified like:

protected $rules = [
    'user.name' => ['required'],
    'user.email' => ['required', 'email']
];

Could you try this and comment if it works?