Sessions is temporarily moving to YouTube, check out all our new videos here.

Designing Immutable Data Flows in Ember

Jorge Lainfiesta speaking at EmberFest in October, 2017
58Views
 
Great talks, fired to your inbox 👌
No junk, no spam, just great talks. Unsubscribe any time.

About this talk

Behind an app using immutability there's a happier team that doesn't have to deal with observers, side-effects, identity checks and instead enjoys easier object construction and testing. Adopt immutability in your ember apps today!


Transcript


Hello, everyone. I am Jorge Lainfiesta. I work for This Dot. And this beautiful mediaeval map, I am from Guatemala, this is my country in Central America. But now, part of my heart is in India because I've been working there most of the time this year and I really came to love the place. Actually, people think that I am from India so that's a big plus into being part of the country. Currently, I am living in Austria, as I am studying a master in digital communication and working at the same time. Currently in Southberg so in case you are around, you can visit me or something. So to start, we have to, I wanted to start with something that was not code-ish because we're gonna have enough technology in our life. Because these days, we don't have time to think about the foundation of reality. But before 25 centuries ago, the Greek did not have streaming, they did not have JavaScript, so they had to spice up their life somehow. So what they did was asking questions, very hard questions about how is it that our world works. And one of the hot debates of the time, instead of being whether we use beam or we use something else or whatever framework keeps coming, they had this discussion about what was, whether reality was something that was constant over the time, or if it was something everchanging. So that brings us to the debate of Heraclitus versus Parmenides. So these what these people said is that Heraclitus proposed that what we see today, we'll never see it again. So his famous saying is we never see a river twice because the river is always flowing. Well, in the other hand, we have Parmenides. He says that everything is static. So whatever we see today is the same. There's just superficial changes. So that debate, it's still an open question today. Nobody can actually give an answer like this. Through the centuries, there's like more subtle differences, but these were the two radical positions. So why are we talking about this? It's because I want to introduce the idea that there are two ways of thinking about how we build data. And one of them is when we take the ember object, we could have it as a Parmenides approach. Because we're gonna have the original object all the time. And we access that object that is always there using some sort of setter and we just getters, we actually use object mutations. So the object will always be the same object, and that enables us to have computer properties and many other nice things. But the talk that I'm here for is about an Heraclitus way of doing things, which would be immutability. Because we will never see the same object again. We will always create a new object, just as in the same, we never see the river twice, we'll never see the same object twice. You're gonna be changing and flowing it constantly. So because of this constraint, we will have to think in a different way. It's not gonna be the same way. And we'll see how that changes. So why should we even consider doing immutability. We know that it's like a trending topic. Everybody is doing immutable data structure, other frameworks are all over it. And it's simply because the changes is way easier when we use immutability because we can just do shallow checks instead of having to check for content, and that brings some other advantages. But it's also because it makes the state management simpler because it doesn't have to, you don't have to think too much about whether you're gonna cause a conflict because you have two instances that share a state, so you both modify at certain times and then things go weird on the other part. So that's why it's way simpler to handle state with immutability. So today we'll talk about how we can design some immutable data flows in Ember. So let's start by looking at a very, very simple example of a basic form. So we have text fields. Each text field has just about, and we're gonna build it first using the data down actions up approach. So we could have a component that looks like this that is called user form. This is the template. And here, we're just gonna use the one way controls because that way, we avoid mutating our object. And the interesting part here is nothing. This is just the average template. And then we do have an action that will, in this case, because this is a component, this will just send a signal to the parent saying hey, somebody change this field. Do something about it. So it's gonna send it up and then the parent is the one who is responsible of actually performing the change that the form has. So this would be a natural approach, to mutate the object the lives in the parent by using a set. So that's very straightforward. We just get in the parent, a responsible parent, we get the field and the value and we just set that into the object. Now, we can already start doing some kind of immutability at this same point by, for example, copying the original object and then applying the same, just setting it there. But that also means that we also have to set back again into the context, that new object that we have. So this is not exactly immutable because if we look up with the rigour of computer science, we are here making a mutation to an array, to an object after we create it, so that's not really immutable, but at the end, it's the same. But if we really wanted to make it immutable, it's not really difficult, it's just one step farther, it's just about duplicating the object and then overriding one of the properties with the field. So from that, we can see that applying immutability is just one step farther, we can think as one step farther from data down actions up. So it's nothing extremely difficult or weird. So that's one point. Now, this is something that I have some kind of trauma with because I worked with React and Typegrid for a while, and that messed up my mind somehow. And now every time, like, I work with a component, I feel the need to write a contract on what properties I'm gonna get and what's gonna be the state of my component, so for me, it's really useful to think in these terms. So this is something that I proposed, something that's very open, but something that is useful is that we could create a contract that is gonna be consistent through all our components that use immutability. And we'll explore the advantages of this, but it's really a simple rule. I have a terrible memory so doing things the same way helps me go through life. So what we're gonna do is to say that each component that has immutability is gonna receive one value and it's gonna send an update in the same form. So the previous example that we saw does not follow this pattern. But the interesting part here is that we don't have to worry about what we get and what we have to send back because we always know that whatever we get from the parent, we have to send that off. So if we get the string, we send the string. If we get an object, we send a new object with the update. So that way, we can start building the components. So if we were using Typescript, we could describe this contract as its stated here. With a generic type, whatever we get as value, that's the type that we're gonna send up. So let's transform the example that we just saw to use this contract. Because this form receives an object now instead of sending up a description of the change, we will send up an object that can be applied on the parent. So we create the new object, and we're gonna update the parent with the object that contains that field and the value that was updated. So now we have homogenous types going in and out of the component. And then in the parent, we can still apply the same pattern of just merging both objects by using the structuring process. Now I wanna still create the new object and that is something that we have to set on the context. So that's essentially the same thing. Now, the way that's gonna look when we use it is very straightforward because all our components are gonna follow the same contract. So we will have a value and we provide something. And for an update, that action, we know, that will receive the same object form that we are sending down. So there's gonna be less guesses to do whenever we write code and whenever we use our components. So imagine we have these kinds of layouts in which we have a component that is composed by other components. And then one component uses two other little components. If we were to keep track of where to use certain things or how do these parts behave together, it would be very confusing to keep track of when do we have to mutate, or when do we have to create a new object to update the parent. Because when we're working in immutability, the important part is to update, if we make an update in an object, it has to propagate up to the topmost parent. So we always have to, we always have to create objects to the new parent, new parent, new parent, until we get to the top because that is the signal that says okay, this changes, and the whole thing starts changing. So it's difficult to keep track of as we grow on the complexity of our app. So in this layout, the parts that could be possible is that we get some original data before from the server or from the parent of this component. And then we're gonna split up the data and send it as each component needs it. So if we follow the approach of always receiving something and updating it with the same thing, we can then think of the components in isolation, so we don't have to worry about whatever it gets or what is apparent. So that enables to do more independent components because we always just have to worry about getting an object, we send a new object up, and we don't care about what happens outside. Because where it's gonna occur, it's the parent. Because the parent knows that what it received is a new object. So it can merge it or append it or whatever it needs to do with that. So that is the advantage of this approach. Now, working with arrays is another interesting topic. Because so far, we've seen that we can perform shallow copies of the objects by doing the three dot operator. But that is gonna be a shallow copy, that's why, oh, that's something that I forgot to mention here. The interesting part of doing, also this part, of doing the contract of sending up whatever we receive, is that we can compose the component in such a way that we can build the tree back by having the parents compose the original object from new objects without having to see the whole picture, because each little part will take care of making its own new object. So that's another advantage of this. Otherwise, there are libraries that are capable of tracking down the path of the object and creating new arrays to that point. But that is a fairly complex process of calculating which objects have to be recreated and which objects have to be copied to make it more performant. Now... With the arrays, the key part, as you know, the methods that we use. Because what we're gonna be doing is to use the array as the container. And when we update something in the array, we basically have to recreate the array because we need to update as a new wrapper because we need to change the pointer whenever some of the items in the array are changed. So it's important to know which methods we can use. So it's fairly simple. There are methods that perform mutations and there are methods that do not perform mutations. So it depends on that if there's no mutations, it's gonna be easier to work with. Like the map, the reduced filter, find. We can also use slice to create that shallow copy, again. Or just destructor the array as well. And all of these will create shallow copies of the array that we can then rearrange however we want. However, we have to have a special care with the method that mutates the array. For example, the splice, which is something that can easily be avoided because there are simpler ways of deleting something in the array. The sort, push, and other, basically, all the methods that the table array has, precisely because it's built to mutate the array. So we should probably avoid that if we're going for an immutability approach. Now, the interesting part is that for arrays, we can create and compose the arrays in a different way, and that's very interesting because it keeps us thinking, making us more linear form, and there's less magic involved, but it's still interesting. Like for example, if we want to add an item to an array, we could simply grab the array, destructor all the elements that it has inside, so the three dots that we see there will basically get all the elements, and then we just append it by adding it at the end, append the object by adding it at the end. And we just wrap all that in a new array structure. So that creates a new array with an additional item at the end. So that's something interesting. To remove it, we can follow the same logic. The interesting part here is that this code doesn't work completely unless we assume that we will not remove the first or the last element. But for simplicity, this is, this works. Because let's say we have a to do list and somebody click on delete button, we just have to get the action that says oh, I have to delete the second element. So then I can go to there and instead of performing a search, I can do find position, I'm I'm just gonna go and now copy all the elements except the one that I want to remove, and put all of them in a new array, and that would create a new array. We can say faster because it doesn't have to do any searching, it's a very native method. And then on the parent, whoever receives this new array, we just have to set the new reference of the array or do whatever the parent is expected to do with that array. Whether it's saving it or processing it or whatever it has to do. But it just has to set it because it's a whole value array with a new reference and the new value that it has. Of course, this has some disadvantages as well as the control that we have. Because we just received an array that we know it's new, but we don't know what change was applied to the array. That could be tackled by providing more parameters to the parent. But anyways, that's up to the case of each person. Now, to, this is also something interesting. The way we would use this component that uses an array is the same way as we would use an object or a string or whatever we use. So that makes this interesting because we can easily compose the component and start thinking about what we're sending down and what we expect to get back. So that means that we can share methods across the components in a single parent to mutate the parent state. Now, another thing that changes are computer properties. Because remember, immutability is not, not the way it was thought at the beginning. So there are some considerations that we have to make about the features, especially of computed properties. So there is even a comment on this on the Glimmer documentation. Because it's stated that there are two approaches that frameworks have taken over updating things. One of them is to have a granular observation of the property and then updating it. And then the other one is just making a shallow check and just changing it. So for computed properties, normally, what we would do is just specify exactly what we want to observe. So if we want to observe the name and the surname of an object to reevaluate the property, we will do it like this. But now, because we're using immutability, that means that it doesn't matter if something changes because the root reference change is gonna recalculate the property. So this has, again, advantages and disadvantages. Because even if a property that is not observed here changes, it's gonna retrigger the recomputation of the computed property because it's just gonna detect that there's a new object, okay, we have to recalculate everything. So it's gonna trigger that for all the computer properties that observe them. However, that also means that we don't have to specify their entire path because we just have the reference to the object. And if the object changes, then all the computer properties are gonna start recomputing. So that's something to take in mind. Now, one important thing is that this approach is vry, very nice with Glimmer. Glimmer AS, or Glimmer 2. Because there are new opportunities in Glimmer and the way it works. Because Glimmer introduced the concept of tracked. So that is an annotation that can be used two ways. The way Glimmer works is that it's different because now, the assumption is that the properties will not change unless you annotate them with tracked. So you just say a property is tracked, that means that it can change. Otherwise, it's not even gonna bother to look for changes. So that's interesting because now, we can have very few properties that have actually been tracked. And that makes it more efficient. So we can see an example of how tracked is used in Glimmer. Here, we have a property that is called user. And the user is tracked. And the interesting part is that the whole, whenever we change the object, it's gonna trigger an update because tracked can be used in two parts, it can be used in two forms. Either to track a property or that it specifies that a certain getter is tracking, as well, a property. So in this case, user is a tracked property, and we have some kind of computed property called full name that is gonna be revalidated whenever user changes. However, these tracked properties also perform just shallow checks. So that means that the reference for that object has to change. It's not gonna retrigger if I change the name and don't change the object, because that doesn't detect a change, it's still gonna be the same object. So now we have to actually apply immutability. So this is a different paradigm because now, we are encouraged to actually do immutability. Because otherwise, we will not get the expected results. The other interesting part is that we could have the whole state as a tracked property, so whenever we update that, that state as an immutable object, it will retrigger whatever we have to retrigger. So the way we would update the thing is the same, essentially, it's just getting the user from the context. Then we apply the mutation that is coming up. And then the tracked property is gonna automatically propagate those changes and recalculate the properties. So the way it would look is something like that. This is how I used it yesterday. I didn't know there was a new change on the way, the syntax of the components. Now, what could be the future for this. So if I could read the future, I probably wouldn't be here, I would be working on the stock market and I would be very rich. But I cannot, however, I have some comments to do this. It's a very broad topic, and it's the performance of this approach is not particularly faster in Ember, especially if you're using Glimmer 1. I remember I was working on a project recently that was using Ember 2.8, and it was extremely slow because we updated the state up to the parent. So that means that it had to re-render the whole component change up to the bottom. So there was a form in the middle, so if the user was inputting something, and the change happened, it would re-render and focus out the user. So it was very messy and we had to roll back into normal object mutations. So you have to keep in mind that this is kind of a little bit not very performant yet in older versions of Ember. But with Glimmer 2, for example, it works very smoothly. So it's up really onto the development strategies that it provides. So it's important to calculate the trade off. So if this is actually gonna make our team more productive, we could go for it because there are better libraries coming up. We have, like, the mainstream library, immutable JS, that tries to perform smarter immutability patterns, rather than, like, copying everything. It tries to do like lacy things and stuff. But it still... So another interesting part about these libraries is that they constantly go into abstract, in a way, the immutability concept itself. So at the end, the end user doesn't actually notice the immutability part. Even, whenever you use redox, when you use that action from redox, you know something behind is doing something immutable, but you don't have to think about it. So maybe we'll get to a point in which developers don't actually think about immutability, or that the browser supports smarter moves, and we don't have to think about these topics. So that's why I started the talk with the Parmenides versus Heraclitus debate, because at the end, it's an open question that as technology keeps evolving, we might end up back without immutability because the browser handles it for us, for example. So that's basically all I have to say. So thank you, everyone. Keep in touch.