Using the Proxy API for State Management


With so many libraries to choose from for state management, why not add one more? The ECMAScript Proxy API enables you to intercept and redefine how an object operates. Let's explore how you might use the Proxy API for state management!


♪♪ ♪♪ ♪♪ ♪♪ Hi, I'm Will Johnston. I'm a developer advocate on WP Engine's DevRel team, and I'm here today to talk to you about how you might use proxies, or the JavaScript proxy API for state management. Proxies, if you're unfamiliar, are a way that you can observe some of the underlying operations on objects within JavaScript. So if you're familiar with object.observe, which doesn't exist anymore, but that was kind of the precursor to proxies, and if you're also familiar with property descriptors or object.definedproperty, that's a similar thing to proxies. Proxies just enable a little bit more full features. So a little bit about myself. I have been writing code since I was about 10 years old. In high school, I got a job doing some web programming with Python, and I really fell in love with software at that time and software development, and I've been a constant learner since then. I spent most of my career working on the web and backend, and over the past few years, probably six to eight years, I've been working mostly with Node in the JavaScript and TypeScript ecosystem. So I really got interested in proxies because I've done a fair amount of.NET and C Sharp code, and in C Sharp, you have a concept of getters and setters on an object, and it allows you to stealthily poke into different operations on that object, and proxies function in a similar manner, but for JavaScript. So I got really interested in proxies to try to figure out how I can make that work because I really love when you can just use objects as you normally would, but underneath, you can understand exactly what's happening. So there are plenty of proxy methods, and proxy methods are used to set up traps so you can observe some of the inherent ability for JavaScript to be performing different functions. So that's what a trap is. We're going to use the get and set traps today because those are simple and easy to grasp, and I'm not trying to go too far in depth with the short time that we have. Something to know, when you're using proxy, you really also need to be using reflect. So the reflect object is used to forward all the operations onto the original object. So if you think of a proxy is in front, and you're listening to the set method, when somebody tries to set, you want to use the reflect object to eventually forward that operation on and actually set the value on that object. The reason you do this is related to avoiding this and inheritance issues. So if you create a proxy and then you try to inherit from it and you don't use reflect, you're going to get some wonky functionality where you're actually changing the original object, not the inherited object. So let's just start with a basic demo. Let's get a state object up using proxy to observe changes to that object. So we will, I like to create a proxy handler. It's the easiest way. I know that I'm going to eventually have to create a new object, a new proxy, and we will create an object. And our handler is going to be our proxy handler. So we know that our proxy handler is going to be a type proxy handler. And that's where we have our get and our set methods. So let's create those now. Target property and set target property value. So the get method just returns the target, the property at a certain target. And the set method sets the value of a property on a target. So in here we'll say getting property. In here we will say setting property. Oops. And we will use the reflect API. And this is how we can, it just mimics the proxy API directly and allows us to not have to worry about what actually happens underneath. We just want to poke in where we care about it and observe the change. There's one additional piece we care about here, and that is called the receiver. And we want to add that in there. The receiver is going to provide the reflect object with that proper context. So here we have our get, our set. We are just intercepting these operations and logging. Pretty simple. And for this let's create on our state a person. We'll say first name Will. And let's go ahead and just add a last name too. Johnston. All right. And then in order to actually see what's going on here, we'll have to console.log state.person.firstname. So what we expect here is it's going to log getting person on here. You might expect it to log getting first name as well. We'll see whether it does in a second. Spoiler alert, it does not. And I'm just going to say here, and I'll call William. So we'll try to set the first name. And that's good. All right. So note, when we create this proxy, we can just use state like a normal object. We don't have to care. Typically, you'll use a library that might be using proxy underneath. You end up with an object that you use as just a plain old JavaScript object. You set values, you get values, and you don't even know if there's a proxy underneath. And that's what's really nice about proxy is it's very stealthy. So let's see what this does here. Node. Oh, receiver is not defined. Let's add receiver in here. Boy. All right. Getting person will setting person. So note, getting person here, it didn't say getting person and it didn't say getting first name. And then in here, we set person. It didn't say setting first name and it didn't say setting last name. In order to do that, we would have to have a way to recursively observe all of the properties on this object. So let's look at what that entails here. In order to do that, we would need to know to keep track of all of the different properties and really the path from the original target to those properties. The other thing we will need to keep track of is whether we've already set a proxy on an object already. Because if you try to create a proxy on something that is already a proxy, you'll get an error. And we don't want to deal with that. So we need to create a couple of caches. Let's do a path cache. We'll use a weak map. And a proxy cache. We'll also use a weak map. So in here, this is fine. First, let's get the value. Now, in a real world scenario, we would have to do a lot of checks on this value because you don't want to end up creating a proxy on a primitive object. So like strings, numbers, boolean, etc. But for the case of this demo, we're just going to check type of value is a string. If it's a string, we'll just return value and be done. If it's not a string, we need to get the path. So if this target is already in our path, which it should be, we will get the path here. And then we need to augment the path. So first we're going to say if the path is empty, so if target is our original top level target, it will have no path. So then if the path is empty, let's add a dot. This is just dot notation, so we're going to end up with a path with dot notation. And then we're going to create a function that will build our proxy. We'll pass in value and we'll pass path and property to it. So that will let us build our proxy. I'm not going to worry about set for the purposes of this demo because it's a lot more complicated. But let's just worry about get so we can observe whenever we're trying to get a property. So let's build this build proxy function. Build proxy. And it takes in value and property. Property is going to be that path. The value is going to be the target on which we want to create the proxy. So the first thing we're going to do is set at the path cache. So that up here we can actually look up all the paths. So we'll always know the path and we'll be able to recursively build this path and this cache of paths. And first we're going to use this this target to check if we already have a proxy. So we want to look in the proxy cache for this value. And if it exists, then we don't need to do anything. So we'll say if not proxy, we want to or really let's be more explicit. If proxy is undefined, first, let's try to create the new proxy. We're going to use the same proxy handler that is our recursive proxy handler. Let's not worry about logging anything in the cache. And then we have this proxy, so we need to add it to the proxy cache. So proxy cache dot set value proxy. There we go. Now we have defined that we're keeping track of this proxy cache. So it's going to have all of our proxies. So we don't accidentally overwrite a proxy with another proxy. And we have this try catch just in case something goes wrong. Okay, so we built our proxy, we have our getter that's recursively calling build proxy. At the end, build proxy returns the proxy, which is going to be the actual value. But what we need to do here is instead of new proxy, we need to say build proxy. And for now, let's just initialize it with an empty object. And then we'll say state dot person equals first name, last name. And yeah, this works. Okay, so we're going to build our proxy here to start. Then we're going to say state dot person equals first name, last name. We will expect something to be logged here. And then we're going to set person again and something will log again. So we got person, that's right here. And then we got first name. So getting first name, this is awesome. We have this recursive getter. So every time we get a new property, it builds a proxy with that property so that we can observe all of its children. And so we were first able to observe state, then we were able to observe person, then we were able to observe first name. And it terminated at first name because first name is a string. So if we had something off of first name, that wouldn't work for us. So let's look at a library that I wrote called Taits. So Taits is a small library for state management that uses the proxy API in a similar manner to how we're using it here. But it allows you to just create your proxy. It does everything it needs to, and then you can subscribe for changes to paths on that proxy. So I'll wipe this out. I've already installed all of the dependencies I need. But what we're going to do is use Taits and fetch posts out of a headless WordPress site. So we'll use Apollo. We have a headless WordPress site set up with GraphQL. And we're just going to pull in posts. I'm going to copy and paste some stuff in here so I don't type it wrong. But we're just going to use Apollo and make a GraphQL call to get posts out of our headless WordPress site. And then we will set those posts on state and try to observe the changes. So the first thing you do with Taits is you get a state object and a subscribe method. And if you're wondering why I call it Taits, I'm not very original. Taits is just state with the S on the end. So let's subscribe to posts. So we want to know when the post object is set. And posts. We expect posts to be an array of posts. So let's just say if not array.isArrayPosts do nothing. We only care about if we have posts. And then let's log there are posts.length posts. Okay. So when we set posts on state, it's going to log the length of the posts. So by default, WP GraphQL pulls down 10 posts. And I'm aware that the site we're going to be calling calls 10 posts. So let's take this one step further. And let's actually in a loop, let's observe all 10 of those posts. So ahead of time, notice our state is nothing. We haven't defined posts or anything on our state. But we can observe changes ahead of time. So let's subscribe in a loop and just try to get the title of each post. So if not title, return. So in order to do this, we want this for loop. And then we want posts.i.title. Taits is able to turn this into a map and understand we need to listen to, you know, posts as an array. We want to listen to property 0 through 9 in that array, or index 0 through 9. Okay. So now let's create our function to grab posts. And I'm just going to copy some of this in here so that I get it right. Yeah. Let's do that. Get posts. Because otherwise I will not get this right. So if there are errors, we'll just log them. Okay. I have a WordPress site set up at this URL. I'm going to use isomorphic fetch because we're using Node here. And my WordPress site already has a list of posts, so it's ready to go, ready to get posts. And I'm going to copy the query here. Client.query. So we're making a GQL query here to get posts, posts, nodes, title. This is just the WPGraphQL API. And then we will set posts to result, or request, So assuming we're able to make this request, get the posts, we will set the array, which is these nodes, and each node has a title on it, to posts. All right. And the last thing to do here is call get posts. So let's see, let's run this and see what it gives us. It gives us nothing. Great. So what did we do wrong? Let's go back through here. I wrote a lot of code without doing much in here. So what have we done? Ah. We need a wait. There we go. So with our wait, it actually waited for the posts to return. And then we have there are 10 posts. And for each post, we're logging the title. My titles are very original. Post 41, 40, all the way down to 32. And there you have it. We have successfully imported Taits in a more real-world-ish scenario. We're calling data. We're getting posts from Headless WordPress. And we're setting state, and it's updating us and our subscribers. What I like about this is we don't have methods specifically for setting. We just set posts directly on here, and all of our subscribers were updated. So you don't have to worry about it. If you like this talk, then you can get all of the code for this out on GitHub. If you go to slash wjohnstow, that is my GitHub, and I have it hosted out there. You can follow me on Twitter at wjohnstow. And there's going to be a little Q&A, so I can answer any of your questions there as well. Thank you, Will, for this great talk. Now let's see the poll. What do you prefer, TypeScript or JavaScript? So TypeScript by 56%, and JavaScript by 44%. This is quite interesting. Is that surprising for you, Will? Yeah, actually. TypeScript started out in a huge lead, and then JavaScript crept back up. I think that, I mean, obviously TypeScript won handily here. But somebody was mentioning in chat they consider it a loss because TypeScript didn't win 90% to 10%. Which I tend to agree. That's funny. I voted, in full disclosure, I voted for TypeScript. So I was trying to move from here a little bit. In full disclosure, I voted for JavaScript. So we canceled out. Nice. Yeah, we canceled out. But do you think, for example, that maybe TypeScript can replace JavaScript? So I think that TypeScript will probably never fully replace JavaScript because there are people who just love JavaScript and love the dynamic nature. And I mean, more power to you. I think when I build large applications, I see the real benefit in TypeScript. But sometimes if I'm just trying to write small scripts, JavaScript is good enough. Yeah. Yeah, I agree with you. I don't think TypeScript can replace JavaScript in the near future. But it's huge. And I think we'll keep increasing and getting more adoption. But JavaScript will always be number one leading. Okay. So now let's go to the Q&A if you have questions. So the first question is what are the performance implications of proxies? Yeah, so a lot of people bring up when talking about proxies, they bring up the fact that the performance of proxies is slow compared to just normal JavaScript. To that, I would say it really depends on what you're doing. But if you are working and contemplating something like replacing all your promises with callbacks or something of that nature because you're trying to eke out that extra bit of performance, then maybe you should avoid proxies. But proxies should be fast enough for normal use. And somebody mentioned it in the chat, but there is another state management library called MobX. I don't know how people pronounce it, but it uses proxies under the hood as well. Yeah, so it doesn't affect performance very much. Okay. The next question is from Oli. And if anybody has questions, please go and community track Q&A and ask questions to Will. So the next question is it looks to me as if Tate does something similar to MobX. Can you point out the differences? Yeah, so they are very similar. I'd say MobX is a more full-featured state management library. The reason I wrote Tate's was for a few projects that I was working on where I wanted to have some level of state management that was more than, say, just using React's context and state. And I didn't want to go as far as to use MobX and learn all about that. I just wanted a very simple wrapper around proxies so that I could get some data and really handle things in a more centralized location. Okay, thank you. Thank you for your answer. So it seems that we don't have any more questions. But if you have questions, please let us know. You can contact Will directly on the Discord channel. I guess he's also on Twitter. Oh, there's one other question. Could you share GitHub link for code? Yeah, so there's the GitHub link where you can follow along. And this is the code for the talk. And in there you'll find links to Tate's and all of that goodness as well. Okay, nice. Thank you very much. This talk was great and the questions as well. So thank you. Bye. Bye.
27 min
09 Jun, 2021

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic