Help Me Solve The Hardest Problem In Livewire (And Maybe The Entire World)

There is a problem in Livewire that has plagued me since the beginning. I thought I solved it a couple months ago. Turns out I didn’t, and now I don’t know what to do. SO, I’m turning to YOU to help me out. I’ll try to make the explanation of the problem as simple as it can be, but as thorough as it needs to be. Thank you in advance for spending your precious brain power on this.

The Problem In One, Small, Code Snippet

class ShowPost extends Livewire\Component
{
    public $post;

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    ...
}

This little code sample seams reasonable. It’s one of the first things someone new to Livewire might try to do. HOWEVER, it’s illegal and will throw an error.

Why is this? Here’s the short explanation:

Public properties are sent back and forth to the browser on every update AND the browser has to be able to interpret their contents. This means, all public property data MUST be JavaScript readable. PHP objects ARE NOT JavaScript readable.

Now, here’s the full explanation:

Consider the following Livewire example:

class Counter extends Livewire\Component
{
    public $count = 1;

    public function increment()
    {
        $this->count++;
    }
    ...
}

When a user hits the imaginary plus (increment) button, an ajax request is fired off to the server that looks something like this:

URL: your-app.com/livewire/message

Payload: {
    action: {
        'type': 'callMethod',
        'name': 'increment',
        'params': [],
    },
    data: {
        count: 1,
    }
}

The server then does the following things:

  • Creates a new fresh instance of your component class
  • “Hydrates” that component with the data from the payload (SETS the public property values)
  • Runs the “increment” method on the component (updating the count to 2)
  • Renders the Blade view of the component
  • “Dehydrates” the component (GETS the public property values)
  • Sends it back to the browser

Here’s what the response payload looks like:

Payload: {
    html: '<div>...</div>',
    data: {
        count: 2,
    }
}

Livewire then uses a package called “morphdom” to intelegently update the current DOM to match the returned HTML. (The user will see the number increment.)

If the user clicks the increment button again, this process will rinse and repeat.

Ok, now that you basically understand exactly how Livewire works. Let’s illustrate the problem with this new knowledge.

Consider again the original problem snippet:

class ShowPost extends Livewire\Component
{
    public $post;

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    ...
}

Now, what would the data part of the payload we described earlier look like for THIS component.

Payload: {
    post: ???
}

Maybe post is just $post->toArray()? And we could program Livewire to automatically turn this data back into an Eloquent model on every request.

Payload: {
    post: { id: 1, title: 'Some Title' }
}

OR maybe we serialize the object using PHP’s serialize() function.

Payload: {
    post: "O:8:"App\Post":26:{s:11:"\0*\0fillable";a:1:{i:0;s:5:"title";}s:9:"\0*\0hidden";a:1:{i:0;s:10:"created_at";}s:13:"\0*\0connection";N;s:8:"\0*\0table";s:5:"todos";s:13:"\0*\0primaryKey";s:2:"id";s:10:"\0*\0keyType";s:3:"int";s:12:"incrementing";b:1;s:7:"\0*\0with";a:0:{}s:12:"\0*\0withCount";a:0:{}s:10:"\0*\0perPage";i:15;s:6:"exists";b:1;s:18:"wasRecentlyCreated";b:0;s:13:"\0*\0attributes";a:4:{s:2:"id";s:2:"39";s:5:"title";s:4:"yeah";s:10:"created_at";s:19:"2019-05-20 21:01:49";s:10:"updated_at";s:19:"2019-05-20 21:01:49";}s:11:"\0*\0original";a:4:{s:2:"id";s:2:"39";s:5:"title";s:4:"Some Title";s:10:"created_at";s:19:"2019-05-20 21:01:49";s:10:"updated_at";s:19:"2019-05-20 21:01:49";}s:10:"\0*\0changes";a:0:{}s:8:"\0*\0casts";a:0:{}s:8:"\0*\0dates";a:0:{}s:13:"\0*\0dateFormat";N;s:10:"\0*\0appends";a:0:{}s:19:"\0*\0dispatchesEvents";a:0:{}s:14:"\0*\0observables";a:0:{}s:12:"\0*\0relations";a:0:{}s:10:"\0*\0touches";a:0:{}s:10:"timestamps";b:1;s:10:"\0*\0visible";a:0:{}s:10:"\0*\0guarded";a:1:{i:0;s:1:"*";}}"
}

That would make the data unreadable to JavaScript (for handy things like dirty checking). But let’s forget about that, it also exposes server-side code to the browser, which is kind of a deal-breaker here.

Ok, so what if we encrypted it using Laravel’s encrypt() function (which serializes, then encrypts it).

Payload: {
    post: "eyJpdiI6InYraldHU1JlcDNZcTBXdElZSDVSa1E9PSIsInZhbHVlIjoiOHU4UVkzcm4zVHNUQWRrcUxnRTZxWWpwRUJ3MEZ0Mm1ZaVpEZHhaU2E0ZkNDdG53NlwvWE5QVW5XQVhRRytWTFlyZEZYQTdBMjExTDFZQSt6S1JcLzJoWURHazdsbGwyRzNFSlZjWXFSV1BXV3c5ODNFZlhaaEQzXC9KcGI4cEVDR0VldmQyS2hMdnY0aXg2cXE0d0tLdmVSaVFvUklDSnlndldkXC96M1l6YWFaVDVxVmNVNzlwWXlsNkNHTGlHOUtReEQxM090enRDK2swR3plWXM2a01SNXRJRU1kaGNMd1dpNHBrR1FmSWZ3UXM5Nkg3MHpUWHdROUFXYVwvWkRVQTI1UGd1dHRIUFZkXC9vOVRGdDlZMjcrSzZXcmYrVldxYU9NS1JpWTZqOFpSWDNEQnl6RjM0cVFGcmlsXC83K2hmWEZKcDBpSU00SW03SXhYb3VIemJuTlBSOFVJWEtmMVYyU1wvSWtkOEtkQXh0dnM4VkdmMmtGZ3VMdE5leGpDZVRQZnRLVFZcLzF0SGd2NzVvWU1ISEhPMFRCYWJRXC90SUVIU2JlbUk4RDVKeHhDaXdnbmF1OFk5RGM5a0pcL0I3dDhoSjVrTGNUbmVzWnJCV2pPZnJ2elIycG1FeHNvemhKeVwvN2UrbGhVXC9RaGZHSDBMcitBcWFNV015dlVTSjN1S1wvUHEwazlTRTVZS25DeFBCRks0Rk82NnhKMXAyblNidEJqc3pTNHMxd2JqeEpJbis2WkxDRWR6SUNjMlp4SytsOW1lK1kzbitVRE9qM1BWWXIxMXI5bWVXWkRPM1wvbmZXdmxZSXN3aXE3TWFLTXFFYmFJVzRyM0hJSEY4eTJhbFRLRHdhSWlYdXdRWlVHUW9TM2s4SmxKY1A3bG0rcGtNRmhsanFkdDNIazBPTDBEQ21uOHNcLzBXaVFuOW5kWHB3MERvNHh1cmZkVHJXTFFrdkhSd1BtVncyVVNMS0pFVEkyUll0NHhOUVJVbSs3ZHY1eHE4TUN4NW9mUnRsaXE5dmV2VWU3MHV6ZmNzQ01HVlwvMHV2TUZwcEFOVVFNcVdsbzhpYmNOaFZLMEcxMUFhM1M2NkdcL2xGZElOUWxxakozQ2dWNFhJNUQxSnoyTTBhSVNKcGM3SjZvQnozNVh3UmNSRVFBYkJcL0s4UHQxOUZvMDI1VEc5Wnozb1FmMFwvdDJ0Z2FXRGhib3hrQ0V3cExhc21NRVpneFJSc3hGSTZvSk5FN3ZOdGk3RUxQUjlNTnlsYk5ZY3oyYm43bGpzc1pRT0RCQ0k2c0dkTG5uVFNGVGZ1M29sTjN3cHVCQSt1Qkd6MTJcL3dKc2RWYlN1c25JUzFNODlCVStIM3RMQnRjK3JZS0paRCtxbnpxN1ZlZDlQRW9xVHREZXhLZXZueWcxaVBnQkVBVXYyUEJ5OEluMFFNM1RpTGhEMUVVYmhkN0RHWFdJd3pSWVdcL2ZyME1NRU42UGNxTnl2dGkzV25pWWZCMXBGdERwQkMyT3lxYXJyT29WcmFvT0VzYnQ4b0pFYzBpalJiUlRkaytBWndobXI4dXRPRlBOMlNJS3pOT1d0WU93dFdJYUdseGxId1JyTjc0RWZJNlpYNUV2TUFsSXY2YThDa0hENGx4WmVMM25JZFF4SmdVSHkwdzhJVk1ONEdzdUF1cVVOaWVjNlBwQ01nM0ZaamZpNlUrWFVpWjN5bGowWGl4UW14MTl5UEUzaW10NHdPekhqZ1pcL2thd0E9PSIsIm1hYyI6ImJkNzM1NjBlMjM2YjQxNWFjYWU3ODBhYmNhODcwNWJhZWViMmZmNGU0MjhhYTI2OTFkMThlOGIyNGVkMTI4NGUifQ=="
}

Ok, this solves the sensative backend data problem, but the payload is freaking HUGE. And this is just for the tiniest example model with one field called “title” and nothing else. Not to mention how slow encrypt()ing is. We could try to make it faster, but we’d be compromising security by using less encryption rounds… ugh…

Ok, so, what if we solve all our problems by not sending the $post data to the browser at all? What if there was some mechanism in Livewire that automatically took things like Eloquent models and stored them in the server-side Laravel cache between requests? This would be fast, secure.

Now that you understand the problem deeply, let’s take a look at a solution I implemented months ago and thought it was the silver bullet (spoiler alert: it’s not).

The Initial Solution In One, Small, Code Snippet

class ShowPost extends Livewire\Component
{
    protected $post;

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    ...
}

Notice the difference? The $post property is protected.

Remeber the flow I described before about what Livewire does everytime an update is made to a component? Well here’s the new flow with the changes highlighted:

  • Creates a new fresh instance of your component class
  • “Hydrates” that component with the data from the payload (SETS the public property values)
  • "Hydrates" that component with the data from the cache (SETS the protected property values)
  • Runs the “increment” method on the component (updating the count to 2)
  • Renders the Blade view of the component
  • “Dehydrates” the component (GETS the public property values)
  • "Dehydrates" the component (Stores the protected property values in Laravel’s cache)
  • Sends it back to the browser

Nice right?

Well there’s 2 issues with this approach. One tiny and solveable, and one MASSIVE and unsolveable (or so I think).

Issue #1

If each person using your app uses a component that stores properties in the cache. Won’t the cache get pretty darn big pretty quick?

Why yes, it will.

However, the solution’s pretty simple. I made a little garbage collection system, that keeps track of when a user navigates away from a page, and let’s the server know which components were on that page and aren’t being used anymore. The server then clears the cache for unused components.

Nifty right?

Well, this leads me to issue #2 (the BIG one):

Issue #2

Scenario: A user is using your Livewire component with protected properties on a page (all is well with the world). Now, they navigate away from the page (all is still well, AND like I said earlier Livewire will detect the navigation and clear the old cache).

The Problem: WHAT HAPPENS IF THEY HIT THE BACK BUTTON!!!

Well, they would get some error, because Livewire is like… “Hey, what happened to those protected properties I put in the cache?”

Ok, so this isn’t THAT big of a deal, we should just adjust the garbage collector to be less agressive and give a grace period for a user to navigate back to a page.

Cool cool cool…

Wait, but when the user navigates back now, the public data will be fresh from the page load, and the protected properties will be left in the state when the user navigated away…

If you didn’t follow this, here’s a demonstration of the problem:

Consider the following component:

class WhackyCounter extends Livewire\Component
{
    public $publicCount = 1;
    protected $protectedCount = 1;

    public function increment()
    {
        $this->publicCount++;
        $this->protectedCount++;
    }
    ...
}

Let’s say this component just shows the user 2 numbers and an increment button.

Here’s the problem scenerio:

  • User sees two numbers: 1 & 1
  • User clicks the “increment” button
  • User sees two different numbers: 2 & 2
  • User navigates away to a new page
  • User hits the back button
  • User sees two numbers 1 & 1
  • User clicks the “increment” button
  • User sees two numbers: 2 & 3
  • WHATTTT??? 2 AND 3??? WHY???

Here’s the reason this behavior is so whacky:

When you hit the back button in a browser, it doesn’t reload the previous page. It just uses the cache it has of the initial HTML state.

That’s why the counter resets to 1 & 1.

The issue arrises on the first round-trip to the server. The backend get’s the public 1 data from the request payload, but the protected 2 data from the old server cache.

There is a data mismatch.

The mismatch is caused because public and protected properties are fundamentally different in nature.

Public properties are stateless (They get passed around, and there can be copies of them), and protected properties are stateFULL (there is only one reference to them that lives in a real place on a server).

There are some big advantages to having a completely stateless Livewire. (pre-fetching, time-travelling in a debugging tool, etc…)

I know I’m throwing a lot at you. Forget about any fancy words I used. Let’s keep moving forward:

What The Hell Do We Do Now? Why Is Life So Hard?

Assuming Livewire should support the browser’s back button (hehe), here are the potential solutions as I see them:

A) “Punt”: Punt on the hole thing and tell Livewire users to go take a hike.

  • For real though, this might be the best thing. Users would have to set the id of a model they wanted to use as a public property and get it back out of the database anytime they needed it in their component. Like this:
  • public $postId = 1;
    
    public function mount(Post $post)
    {
      $this->postId = $post->id;
    }
    
    public function someAction()
    {
       Post::find($this->postId)->doSomething();
    }
    

B) “Model Casting”: Implement a “Data Casting” system that would look like this:

  • public $post;
    
    protected $casts = ['post' => 'model'];
    

    Livewire would call $post->toArray() to dehydrate the model, and newFromBuilder($data) to re-hydrate the model back into an eloquent model.

    This solution is the most interesting to me because it would be user-friendly, but I have concerns about data-visibility, payload-size, and other odd things that would come from re-hydrating an eloquent model.

C) “Computed Props”: Build a “computed property” system and make that the idiomatic way to deal with models:

  • public $postId = 1;
    
    public function mount(Post $post)
    {
      $this->postId = $post->id;
    }
    
    public function getPostAttribute()
    {
       return Post::find($this->postId);
    }
    

    The user could then access the post in the component with $this->post OR in the Blade view as just $post.

    I also like this solution. It WOULD add a database query for every request, but a ::find is generally pretty cheap. It also is less user friendly than other solutions.

D) “Bespoke Caching”: Offer a “bespoke caching” utility in components to store anything you want in a cache that is scoped to an individual component instance (like protected properties currently are)

  • public function mount(Post $post)
    {
        $this->cache('post', $post);
    }
    
    public function getPostAttribute()
    {
       return $this->cache('post');
    }
    

    The user could then access the post in the component with $this->post OR in the Blade view as just $post, just like the previous solution. EXCEPT, now there is no DB query every request. HOWEVER, this would be behave exactly like the current protected properties solution and would therefore inherit the problems involving mixing stateFULL and stateLESS data (data drift on the back button click, etc…)

    I will likely add this caching feature anyways because it would be useful for other things, I just may not recomend it as the idiomatic way.

You Made It! Now TELL ME WHAT TO DO!

Phew, that was a long one. Thanks for following along and understanding every word I said and their deepest implications. You are a champ!

I’m so deep into this problem I’m having trouble knowing the best way forward.

What do you think? What would work best for your use cases?

What would you prioritize? (user friendliness, data obscurity, performance)

What would work for your projects? (Maybe you don’t even need to store models and other PHP-objecty stuff)

Thanks a bunch and ton!

  • Caleb
1 Like

If you call $model->fresh() in the render (or mount) function does that fix the issue?

1 Like

Which issue exactly? Assuming you are suggesting to do that with the “Data Casting” solution - you would be correct. calling ->refresh() would probably be best to do after hydrating the component.

I have a hunch though that I’m way ahead of what you’re actually suggesting, so maybe just break it down for me.

Thanks for joining in!

My first thoughts are caching can be a nightmare to keep on top of.

We’re assuming a user always needs to access the model on every request, but that may not be the case.

Couldn’t you have __get and __set magic methods to store/access the models in the component? and something like $casts but to indicate what properties need to be treated as models?

Then you only query the model when it’s needed, and you could pass around the model ids to the front end in state.

(very contrived example below, forgive me i’ve been writting javascript code all day)

class ShowPost extends Livewire\Component
{
    protected $models = ['post'];

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    ...
}
class Livewire\Component
{
    public function __set($name, $model) {
        if (!isset($this->models[$name])) return;
        $this->modelInstances[$name] = $model;
        $this->modelIds[$name] = $model->id;
        $this->modelClasses[$name] = get_class($model);
    }

    public function __get($name)
    {
        if (!isset($this->models[$name])) return false;
        if (isset($this->modelInstances[$name])) return $this->modelInstances[$name];
        $this->modelInstances[$name] = $this->modalClasses[$name]::find($this->modelIds[$name]);
        return $this->modelInstances[$name];
    }

    ...
}

The key is livewire would have to pass the model ids around internally (not as public properties) and the docs would need to indicate you don’t have to (actually a requirement) declare public properties for model instances.

passing the ids back and forth will have very little performance impact, and then the model is only hydrated when a component method needs access to it.

The more i think about it, you would also need to pass around the model classes too, but again trivial in size if you just passing around class strings and ids.

Payload: {
    html: '<div>...</div>',
    data: {
        count: 2,
    },
    // not sure if this should be in 'data' or extracted as below
    models: {
        'post': { id: 1, class: '\\App\\Models\\Post' }
   }
}
1 Like

Interesting, I really like the concept of only querying the database when the model is needed.

Do you see any issues with sharing the model class path publicly (with the front-end)? Like is there a problem exposing that knowledge about an app’s namespace structure and what not?

Maybe? echo sort of already does that anyway with its notification ids. so yes maybe something to consider, but dont think its been a problem with echo.

https://laravel.com/docs/6.x/notifications#listening-for-notifications

https://laravel.com/docs/6.x/broadcasting#broadcast-name

2 Likes

You could leverage something similar to the morph name when storing polymorphic relationships to obscure the info from the front end. Just seems like a lot of “config”.

but may be worth getting opinions on the class names being public to see if its a concern worth catering for.

2 Likes

2 really good points. Thanks!

Bro I would say to you to forget cache, it will be a nightmare to maintain.

I really like the cast solution, but i think leemason’s answer seems like the most reasonable one.

About the model namespace, I don’t think it is a big problem, but personally I don’t like the idea of my backend being exposed to the frontend like that.

You can assign some alias to the model class like Laravel does with the custom polymorphic types: https://laravel.com/docs/6.x/eloquent-relationships#custom-polymorphic-types

Yeah, I think I’m with ya on the caching thing.

Agreed, the question now is what’s the best syntax for this system.

(For people following along: this doesn’t mean I’m settling. I still want to hear everyone’s thoughts on this weather or not they are inline with this.)

Thanks for weighing in!

By default passing model + id and hydrate when needed.

I think those would none the less make it more enjoyable to work with livewire :slight_smile:

 // keys of props & models to eagerly load on each request
function eagers ($request): array;

// Keys that should be cached as needed 
// Could be skipped on navigator:back for rehydration.
function shouldCache($request): array;

// Prepare a model for being sent down the (live)Wire.
// Maybe $method => Str::studly('transform'.class_basename($model))
// For dynamic transforms, without needing to handle it in a single method :) 
function transform($request, $model)

Could also extract those into a livewire resource class, and simply loop over em, letting the resource decide if it should be cached etc.

I wonder if it would be costly for user friendliness tho… :slight_smile:

1 Like
$models = [
    'post' => '\\App\\Models\\Post'
];
Payload: {
    html: '<div>...</div>',
    data: {
        count: 2,
    },
    models: {
        post: 1
   }
}

could abstract the class name from the front end? then its just if you want magic access to the model via $this->post os something like $this->attachModel('post', $post);|$this->getModel('post');

Tbh i do get not wanting to leak information to the front end, but the raw fact is alot of the time database ids are being passed around which have far more security implications than a class name.

… but …

If it can be abstracted it satisfies those who feel uneasy about it, AND prevents breaking changes during iteration should your models get relocated in future.

1 Like

Is the “need” for this only for type hinted models in the mount method?

If so i’m sure some reflection could do all of this without config required?

given:

public function mount(Post $post, Foo $bar)
    {
        $this->post = $post;
        $this->bar = $bar;
    }

You can work out the class and the property name programatically. The only thing to consider here is users would need to be aware the param names MUST match class property name exactly.

1 Like

I know we are taking the more simple issue here first, which is a model, but let’s not forget about relationships as well. What if I want $comments?

Should there be support for something like this?

protected $models = ['post.comments'];

Where every time the Livewire component is hydrated, you get the related comments as well? Should there be some way to determine if you want the comments to update on every request or only on specific ones?

Sorry I don’t have more answers, just lots of questions. :sweat_smile:

Very good point. i think creating a config structure for all these type of cases could get pretty confusing.

in the concept ive put forward maybe the __get call to rehydrate the model should first look for a hydratePostModel method before using the default logic of model::find.

And then if needed it can fetch related models.

/**
 * Optionally define a `hydrate{name}Model` to fetch the model and 
 * perform any other operations, such as loading related models.
 */
public function hydratePostModel($id)
{
    // the default behaviour: return Post::findOrFail($id);
    return Post::with('comments')->findOrFail($id);
}

All good points.

I really want this API to feel simple, clean and intuitive. Also, I want to offer a flexible alternative for more control.

Here’s the API currently rattling around in my head:

class ShowPost extends Component
{
    public $post;

    protected $casts = ['post' => 'model'];

    public function mount(Post $post)
    {
        $this->post = $post;
    }
}

Internally Livewire will “dehydrate” the model to it’s class, and id. (I still have questions about what this will look like in the payload).

It WONT automatically re-hydrate it, until $this->post is called in the component (I’ll work some PHP magic to make this happen). At which time it will use “Post::find($id)” to hydrate it.

If the user wants anything more (hiding the full model class, adding eager loads), we can do one of two things:

#1: Allow custom “casters” (PHP classes that offer a hydrate and dehydrate method). For example:

protected $casts = ['post' => PostCaster::class];

...

class PostCaster implements LivewireCaster
{
    public function hydrate($value) {}
    public function dehydrate($value) {}
}

#2: Just use the “computed property” api I proposed earlier. (doesn’t exist yet)

class ShowPost extends Component
{
    pubic $postId;

    public function mount(Post $post)
    {
        $this->postId = $post->id;
    }

    public function getPostAttribute()
    {
        return Post::find($this->postId);
    }
}

So, I suppose my question is:
A) Is the proposed default strategy (store the class and id, and rehydrate using ::find) a common enough path to cover the average use case? (I’m fine with just warning them about what’s happening in the docs (exposing the class namespace))