A Lovely Refactor

Nov 08 09:56

After being away for the Livewire codebase for a while, coming back to it was kind of painstaking. Instead of slogging through it, I decided to "make the change easy". Here's the story of a fun refactor I just made.


Okay. I am recording this podcast right after a while. I'm kinda in the midst of a really, really fun refactor. One of those refactors that's just been building for awhile. Something I've been thinking about in the back of my mind. No, I've known that the architecture is wrong, uh, for this specific thing.

And I've just been sitting with the problem thinking about it, trying to come up with the. Trying to understand the problem well enough, come with the solution, yada, yada, yada. Finally did it. And I feel freaking great about it. And it's really, really, really improving the code base, like as we speak.

So let me walk you through it because this is a, it's another one of those refactors like the ones, it actually looks almost exactly like the ones we've talked about before on here where I started talking about like the hooks pattern, the, a single file pattern, like all these things, um, are all culminating for yet another refactor.

So let me walk you through it. It's a, it's fairly simple. So live wire. And you click a, you click something on the front end and instead of it just changing data and rerender during the front end, like few JS does, it goes back to the server and it changes the data. Rerender is a template on the back end.

It comes back to the browser changes the front end, right? That's how live view works. Phoenix live view. That's how Livewire works. Uh, so the difference between Phoenix live view and Livewire is that Phoenix live view keeps a background instance running. So. Where in VJs you have a component that's alive and it exists in the JavaScript runtime over time and live view.

There's a component that exists in the backend, and so Phoenix uses WebSockets to connect to that component and you can make calls to it and all sorts of stuff, and that makes it really fast and it's great. And yada, yada. Livewire version zero, zero one, uh, worked exactly like that, but then I moved away from WebSockets and now live voyeurs, stateless and uses Ajax requests to go back and forth.

So there's no long running instance. Um, because of that, I have to hydrate and dehydrate Livewire components on the back end. So every request that the browser makes, so you click a button in a live wire component, like you have wire click, and then some action. It sends a request off to the server and says, Hey, here's the idea of the component, here's the name of the component, here's the data the component has, and then a bunch of other stuff says, build up that component from all of this, from this state, because the front end stores the state, build up the component from this state.

Then run this action. Whatever you decided on the wire, click that after. Dehydrate the component back into a Jason array of state that I can store because I'm the front end and I have a run time, so I can keep track of the state here, and then the back end is completely stateless. So that's kind of a fundamental, how live wire works.

Little explanation. So I, I decided to explain it again because I know this is the kind of thing that seems, um, obvious to me, but if you're not building live warrior, then you're probably not totally aware of how it works. So that's kind of the ketchup. Okay. So in the back end, there's this concept of hydrating and dehydrating, but when I was programming it, like that concept was never really clear to me.

Um, like I said, it's been an evolution and I'm just sort of updating the code as I go. Um, so there's this, this kind of, this file that I actually like this file. It's one of the most powerful files in the whole system. It's called connection handler. And it's the thing that takes the incoming requests from the browser and it builds up a Livewire instance.

It calls the stuff on it, it gets the new render dominant, sends it back out all in one method. Um, it calls to a couple of different methods, but it's pretty tight. It's pretty clean. And it's, I like it because it, you can get a really good bird's eye view of the backend of Livewire. Well, this class has been growing over time because features get added and bugs get fixed and security holes get patched and all of those things.

And, and the easiest place, uh, to add them oftentimes is right in this connection handler file. So it's starting to grow and there's lots of extra little bits and pieces added to it. So there was a pull request recently too, in live wire protected properties. Tune, tune this part out. If you're not familiar with protected properties in Livewire but protected properties, everything else is dehydrated into Jason.

That gets passed back and forth in the front end and back end. Well protected properties because they're sensitive data. They get dehydrated into the backend cash, uh, and then rehydrate it out of the cash on every request so it behaves differently than the rest of Livewire. So somebody said, Hey, I want to make a pull request so that I can opt into not caching protected properties and encrypting them and sending them with a payload to have a truly stateless Livewire.

So let's say you're using vapor, well, I guess vapor has a cache built into it. Okay, well, so you just want to have a stateless Livewire, then you could do that. Um. So they made the pull request, the pull requests overwhelmed. To me, it's, this is one of those difficult things. Um, I really appreciate that.

I get so many more GitHub issues than I do pull requests. So when I get a pull request, I, I want to be helpful and I want to work with it and pull it in. But it's honestly just a lot of mental overhead for code that I didn't write. So I have to pull it down. I have to read through the pull request.

I have to understand what it's doing. Under and I not only just to say, okay, this works, we'll pull it in, um, to understand what it's doing. So that enough that I can change it if it's not ideal. So I have to really, really understand the code so that I can see areas where, Oh, you know what, maybe a better way to do it is this or that.

And then I almost feel like I'm kind of rewriting the code. Um, I do really, really appreciate pull requests if you're getting the wrong idea. That's not what I'm communicating. But there's a lot of mental overhead, so I'm pretty lazy with them. This one in particular, because it's so big, it touches a lot of files and I realized that I was frustrated and enough that I thought, okay, this is a good opportunity to make the change easy.

So instead of putting my energy into understanding this pull request and making sure and implementing it and making sure that it's right or wrong, whatever, I'll, I'll make the change easy. I'll refactor the code base so that a pull request like this will be. Not many changes to files will be very brief.

Um, then I'll, you know, post back to the, the contributor and say, Hey, I refactored the code base, take a look at it. And, you know, maybe I'll do the work every factoring and whatever I do, I'll give him credit for the pull request. But. Well, basically it's a classic case of make the change easy, then make the easy change.

And this is, as I've said, a thousand times, maybe one of my favorite coding principles ever. Um, so I rolled up my sleeves and I decided to do it. And what emerged was pretty much the hook pattern that I described before, but in a little bit different way. So there is a backend life cycle and I pretty much described it to you.

The back end life cycle of a Livewire component is it gets, I'm going to have to map this out and put it in the doc somewhere. Like if you go to VJs docs, you can see the whole life cycle of a view component. I'll need to map it out for everybody in the doc so that you can kind of understand, um, but live where's a little more complex?

Cause there's a front end lifecycle and a back end life cycle for each component. And I've paid. You know, attention in the front end lifecycle lately, and that's what all those episodes are about, about the hooks pattern and everything. But I haven't really paid attention to the backend life cycle.

So that's, that's where this comes in. The change that I made was basically creating the process of a hydrating and dehydrating a component like I described, where you go from adjacent payload from the browser, and you basically create a component at the state. It was the last time there was a request made and then dehydrate it.

Okay. So I was doing functionality in different places, and basically the pattern that emerged was middleware. So I took a ton of functionality and put it into middleware. I created a little middleware system for hydrating and dehydrating a component. So here's how it works. I'm trying. I tried, this is how I, how I started the refactor.

I know I'm throwing a lot at you, but there's a lot of good nuggets in here. I started the refactor with my second favorite principal, maybe first, I don't know, whatever. Another awesome design code, coding, design principle, um, designed by wishful thinking. This is something I really learned from Adam and I'm sure he learned it somewhere else.

Design D, you know, write the code that you want to use. So that's what I started. I started with just a file. I thought, okay, he's pull requesting this functionality. He should just have a file. He should be able to just pull request one file. So again, maybe another favorite principle is that I talked about is the single file principle.

Um, he should be able to pull, request this with one file that has two methods, hydrate and dehydrate, and the hydrate method. He has access to the on hydrated instance of the component and the whole request payload from the browser. In the dehydrate, he has access to the instance that's already been used and the response that goes back to the browser, and he can pull stuff out of the request and manipulate the instance, and then he can change the incidents and change the response on the backend.

So he gets a slice. It's a middleware. So I did that and I refactored a ton of functionality a ton into these middlewares and now they're well-named and they work like layers of an onion. So like it's an array of just classes that have these hydrate and dehydrate methods and starting from top to bottom.

They get the uh, instance and requests gets passed through them and they can mutate the request and instance as you go through. And this is interesting because, uh, I'm using and leveraging mutability and usually I try any, like good design in my mind is immutable. Like I generally tend towards immutability, but this is an example where with these middlewares it's kind of beautiful.

Like I can have them each mutate the things going through them, um, and then mutate the things coming back out of them. And it works like a shell. I reverse the order when I'm dehydrating back up. Okay. We're at the end of the show. I hope you follow that and thought it was cool, but I'm super duper happy with this refactor.

It abides by all the things I love. Keep more things the same single file principal designed by wishful thinking and make the change easy, then make the easy change. Uh, so thanks for listening.