 Hello, EmberConf. How's it going? My name is Chris, but you might know me better by my online handle, Zuroc. And I work at LinkedIn, and I'm a member of the Ember Framework Core team. Normally, I work on the rendering layer. I work on glimmerVM and components and auto-tracking, that sort of thing. But today, I wanted to talk about something a little bit different. I wanted to talk about a data layer, specifically Ember M3. M3 is a data layer that we developed at LinkedIn. To handle some of the problems we were dealing with at scale with vanilla Ember data. And you might be thinking, if Chris is getting up here to talk about this M3 thing, is this the future of Ember data? Should I switch over to it immediately? The answer is no, actually. Ember data and M3 are built on the same common core and share most of the same infrastructure. M3 is really just a different take on the modeling layer in Ember data. And they can actually be used side by side. If you wanted to use M3, it actually has trade-offs. And there are situations where it makes sense, but there are also situations where it doesn't. So we'll dig into that a little bit more as we go on and see if it really would be the right solution for you. I personally started using M3 in some side projects alongside Firebase. And part of the reason I wanted to do this talk was just, as I was using it, I found out that it was one of the most fluid, flexible, and fun data experiences that I've had in my 10 years as a front-end web developer. Huge thanks to Chris Thobern, RunSpired, for showing me this combination and showing me just how powerful it could be. So, yeah. Today we'll be talking about what M3 is and what problems it was built to solve, how it works, what Firebase is, and why it's a good match for M3. And then we'll actually see the combination in action, so you don't have to take my word for it. First off, what is M3? Well, I think it's best to understand M3 by understanding the problem that it was trying to solve. At LinkedIn we have a lot of data. We have thousands of models, and those models are very complicated with nested fragments within them, leading to tens of thousands of different models and fragments all over. That is very difficult to keep in sync in general with our ever-changing API layer. So our first solution was actually to generate the models. Our API layer is highly conventional, so we were able to build tooling that did just that. This helped with the issue of keeping things in sync, but we still had this problem of shipping tens of thousands of generated models and fragments to our clients. And that added up, that was hundreds of kilobytes of code that was having an impact on our customers. So, enter M3. The idea with M3 was rather than have a class for every single model and fragment, we would have one class that could represent all of them, and it would do that dynamically. The way M3 does this is it really just understands a model to be a blob of JSON. It can have JSON values of any type. So, for instance, here we have an object that has a name, which is a string, and it has a chapter's array, which is an array of objects. And these could be updated to be booleans or any kind of value. You don't have to specify what the shape of your model is ahead of time. Now, this is still better than using fetch because M3 does understand that each model has an ID and a type, and so it'll deduplicate them and make sure you're using the same reference to the model everywhere that you're using it. And this is also very important for M3's other major feature. It also understands relationships within that blob of JSON. It understands them based on conventions. So, in this API response, for instance, we are representing a reference to another object as an object with a type and an ID. M3 will see this, and based on that convention, it will load those models and replace them in the actual model itself so that when we access them, we're accessing the instance of the relationship, rather than the type ID thing, so we don't have to figure that all out ourselves. This can be configured with the M3 schema, so you can make this work with any kind of convention for your relationships and with any conventional API. It can work with JSON API or RESTLY, which is our LinkedIn spec, or GraphQL, so on, anything that has strong conventions for relationships. This allowed us to go from tens of thousands of models being shipped to the client to just one. In our single largest refactor, we removed over 64 kilobytes of generated code after MinNG zip, which is absolutely enormous. So, it's had a huge impact on our code. Now, is M3 the right choice for you? As I said before, there are trade-offs. On the pros side of things, we have the fact that it handles these complex nested models of JSON very fluidly. This also works really well with strong and conventional APIs, and it requires less maintenance to keep things in sync between your front-end and back-end. Finally, for very large apps with hundreds or thousands of models, it definitely can help to improve performance. The cons are, your front-end no longer has your model definitions, so you really have to have good API documentation in order to make sure that your code doesn't get out of sync and that everybody understands what the models should look like, what the API should look like. It also really requires a conventional API. It can't work with one-off or bespoke APIs, although you could use vanilla Ember data alongside it for that. Finally, M3 really expects to load relationships synchronously when you access them. What this means is that there's no promised API for loading a belongs to or has many only when you need it. If you want to do that, you have to make sure that those relationships are loaded ahead of time yourself. So you either have to include that logic in your routes or you have to absorb it somewhere else yourself. And that can be a little bit of complexity to add to your data layer. So M3 is really great for large apps that have conventional APIs and will definitely benefit from the decreased maintenance and payload size. But it's not as great for small to medium-sized apps that won't benefit as much from that decreased size and may have more trouble with the dynamic models and the sync relationships and so on. That said, I can guarantee you that the personal projects that I've been working on aren't the size of LinkedIn and the whole premise of this talk is that Firebase plus M3 is absolutely an amazing combination. So why is that exactly? Well, to understand that, I think we need to dig into Firebase a little bit. What is Firebase? Firebase is a backend as a service, which basically means it provides everything you need to write an application and just focus on the front end. Firebase provides a database and it provides things like push notifications and so on. And this can be really helpful if you're like me and you only have a few hours on the weekends to work on some idea and you just want to get it out there really quickly. Firebase's database is Firestore. And the difference between this database and other databases is that it's a document-oriented database. And what that means is that rather than representing data as tables, it represents it as objects and specifically as JSON. So you can write any record into your database as any arbitrary JSON value. And this has a number of advantages. It means you can represent really complex models very easily. There's no need to have 10 tables and 20 joins to represent something that's conceptually simple, like, you know, an object that has an array with a couple of nested objects in it. It also really means you can think about your database and application in the same way as you're developing them. And that can be really helpful for making application development more fluid. The cons are it has very limited query capabilities. It's hard to query arbitrary JSON objects in general. And because of that, it also has no concept of relationships. Relationships really have to be managed manually. So it means that there's a lot more boilerplate there. You have to do the work yourself to figure out what related values are and to load them. So right off the bat, we can see the similarities between Firestore and M3. Both of them think about models or records or documents, whatever you want to call them. As JSON values, they're just data that has any kind of JSON object within it or array or any other value. The key thing that M3 adds here is those relationships once again. M3 is able to understand what the relationship looks like when it reads that JSON value and replace it with an instance of that class, of that model. So you don't have to do all of that boilerplate to figure out what the relationship is yourself and load it yourself from Firestore. M3 handles that for you. This means you get all the advantages of a document store database, but you have conventions around your relationships. And that is what makes it such an amazing development experience in my opinion. But you don't have to take my word for it. Let's dig right in. It's demo time. Okay, so for today's demo, we'll be working on a simple meal planning app called Ember Meal Plants. And the concept is pretty straightforward. You'll be able to add recipes and then use those recipes to create meal plans. And I've already actually created the recipe portion of the application. As we can see here, we can add a new recipe. We'll call it cookies. And it'll serve 12. It'll create 12 cookies. And I'll say 200 calories of cookie. And the most important part, bake it with love, the most important ingredient when baking cookies. And as we can see, if we reload that all persists in everything. So it's already hooked up to Firestore. What we're going to do is create the meal planning portion of the app, which so far all I have here is an outline made with HTML, but no actual JavaScript hooking it up to Firestore. So we can see we have our index page that shows the total list of meal plans. And then if we go into our meal plan, it has a name. It has a number of days and each day has a number of meals, which we can add to and do things like track our total calorie counts and so on and so forth. So let's get started. First up, I just wanted to have a real quick note. I'm actually using canary versions of Ember data and Ember M3, because I wanted to show a canary feature that is coming soon, which is native proxy support. And that allows us to not use Ember Get and Ember Set and rather instead use native access and native methods like push instead of push object on arrays and such. This is a sorely needed feature because otherwise M3 definitely still feels like it's a little bit pre-octane, but I'm really looking forward to it and I wanted to show you what the development experience is with that enabled because as I said, it should be here sooner rather than later. So first off, I want us to start by thinking about the model itself. So I like to document my models in my code base and I use TypeScript interfaces for that. We don't actually have to create any files or interfaces or any TypeScript to work with M3 models because they're dynamic, but I find it really helpful if there's some clear, concise documentation of what we actually expect them to look like. So I already created one for recipes and we can see here that recipes have a name, which is a string, instructions, another string, number of servings and total calories. So let's go ahead and create an interface for meal plans. Interface meal plan. Export default meal plan. It's a quirk of TypeScript. And first off, our meal plans definitely have a name as we can see here and then they have a number of days and those days are actually going to have a lot of their own properties. So it's a list of objects, an array of objects. And we'll actually go ahead and create another interface so we can document that object and see what it looks like. Within that, we can see days have a number, but that's honestly just the index in the array where the day is, if it's the first one, second one and so on. So we don't need to store that. And they also have a total calorie count, but that's also derived. So the only thing it really has per day is a list of meals. And meals are also objects because each meal has a number of properties. So we'll go ahead and create another interface for meals. And meals have a name as well. Name string. And here's where we start to see some of the power of M3. Meals have a recipe, which we can just import the interface from that file and use it here. And this is really where we see some of the power of M3 because we can just place this relationship on this nested object within this model. So we don't need to create any other model classes or intermediate steps like we would with Ember data. This whole model here is pretty complicated. It's got several layers of nesting and in Ember data, that would have to be a few has many relationships and belongs to relationships and so on. But with M3, it can just be a JavaScript value, a JSON value with the nested relationship in it. So that really helps to clarify everything. And I feel like this interface helps us to kind of see what we expect our data to be and of course to document that. Okay, so now moving on, let's actually hook up the index route here. So I've got my index route set up and I've already injected the store into it. And I'm going to do model. And for the model, I'm just going to return this dot store dot find all meal plan. And that is basically the same as Ember data there. We can just find all the meal plans and use them. And we'll actually use them in our template here. Each model as plan. Each, we've got this link to here. We're going to have to pass the model to it as well. So we'll plan dot ID so that it triggers the logic within our show route, which we'll see in a minute. And we'll use plan dot name for the title of that link. All right. So now we don't have anything because we haven't created a plan yet. So next up, we're going to add an event listener to our template on the button for new plans, create plan. And then in our controller, we are going to add an event listener here for creating that new plan. And we're going to make that an async function. And I've already injected store here as well. It's kind of like a plan this out or something. And then we're going to create a new plan. Let plan equal this dot store dot create record meal plan. And we're going to pass in a name to that and just a default kind of generic name, name, new plan. And we're also going to pass in an array for days. And it's just going to be an empty array for now. So this is again, basically the same as what you would do in emperor data, just store dot create record. And then we can do a wait plan dot safe. And then we'll do we'll transition to the plans dot show route using the plan ID. Perfect. And if we press the button, we see the plan. We don't actually transition to though. And I think I know what that is. I think we need to figure out what to do with first off. We need to add the dynamic segment to our link to. So plan ID. And then we need to actually add some logic to our show route to handle. Loading the data, loading the model. Otherwise there's some default logic that happens and that's not what we want. So let's do that real quick. So we have our model function and like before we have our store already injected, but we also get plan ID now. And I'm also going to import hash from RSVP because I want to return a couple of promises here. So the first one I want to return is the plan. We're going to do this dot store dot find record. And that's going to be meal plan. And we're going to pass in plan ID. So the meal plan that we actually want to load. But then we're going to load all of the recipes that go along that list in our store because we want to be able to select from our recipes within our meal plan editor. You could do this dynamically in one of your components. But for now, just to keep things simpler, I'm doing it here. And I'm going to do this dot store dot query recipe because I also want to pass in an extra parameter here. I want to pass in subscribe, which is going to subscribe us to updates for those recipes. This will help to demonstrate some of the reactive features later on. And this is something that is special to unique to Firestore in particular, it's not an M3 feature in general. All right. Cool. So now if we click on that, okay, our route is working again, but nothing is hooked up of course yet. So let's go to our template and start doing that. We're going to do at model dot plan dot name, but that's not actually going to update anything that header is just there for accessibility and purposes. The value that we actually need to hook up here is this input. And I have an add on that I'm using called X input that we're going to use for that. So we're going to pass in value. This is going to be at model dot plan dot name. And then on input, we're going to set at model dot plan name. So this is using Ember set helper. This allows us to update the value without actually having to define a JavaScript handler, which can be really nice when you just want to update a value. So we'll do that and look at that that works. It updates here correctly, but if we reload, it doesn't persist because we're not actually saving it. So next up, we are going to actually don't think I need store in this one. We're going to add save to our controller. And this is going to take our model this dot model dot plan and call save on it. So then we can use that in our template. I'm going to use Ember composable helpers here to queue up a few different actions to happen together. So we're going to do set model plan dot name. And then we're going to call this dot save immediately after that. So now if we update this to be my plan and we reload, that did not work. Oh, got to save. My plan and everything persists. Awesome. Okay. Next up, I'm going to actually extract out a component here because the rest of this template is a little bit complicated for my taste. I don't think we should do this all in one spot. So I'm going to generate a component. Then I'm going to copy that code over into this component. And then what I'm going to do is we're going to actually loop over model dot plan dot days as day. And we are going to invoke our component meal plan day. We're going to pass it day, which will be the day. We're also going to pass it day number equals and we need our index here from our loop. And we're going to increment it with number of composable helpers ink index one. And for now that's all we're going to pass it. So internally in our component, we can do at day number. Okay, cool. We go back, of course, there aren't any days yet because our meal plan started off with no days. So let's also add the logic for adding a day. We on click this dot add day. And then add day equals this dot model dot plan dot days dot push, we can just push into the days array. Like it's a normal array. We're going to push a new day. It's going to have a meals array and that's all it has. And I also think we can add a default model here. We'll have name new meal recipe null and servings one. Because I think honestly, if you're creating a new day, you probably want to have a meal in it just for starters. So then we'll do this dot model dot plan dot save. And now in our app, we can add a day. And if we reload that day is still there. Perfect. So next up, we are going to hook up the meals form within our day. We're going to add an each loop because we're going to each over the day dot meals array as meal each. And then within that, within that, we're going to use our X input again. I'm just going to copy that over from over here. Put that in. So the first one also updating a name, but of the meal instead of model. Oh, I have to put it inside the ally up here as well. Okay, cool. Perfect. So new meal is updated. Cool. That won't persist yet though because we need to also queue in save and save is not this dot save. We have to actually pass it in as an argument here. So let's pass that in at save equals this dot save. Perfect. Now that should update. And if we reload, awesome. We're still having cookies for breakfast. That's the way I like to start my day. All right. Next up, we're going to do the select here. I'm using X select, which is an add on for selects. And I'm going to do value equal meal dot recipe. And here's where we update and change our relationship. So we can just pass in the recipe, set meal recipe and add save similar to the input. I'm just going to quickly copy over some template for the options themselves. But as we can see here, we actually pass in the recipes themselves into each option and then set them directly on that on the meal. We don't have to do anything special. We can just add the relationship to the JavaScript value and M3 figures out all the details for us. And of course we have to pass in the recipes to our meal plan day components. So we'll go to model dot recipes here. And if we go back now, we can select a recipe. Still doing cookies. All right. Finally, we'll do the servings input there. Just copy our logic here. Type equals number because it's a number input and set meal dot servings. And I want two cookies for breakfast. Oops. Added another day there. Okay. Cool. So everything appears to be working. Last thing we need to do is add this, the logic for this add meal button. So let's do that real quick. We're actually going to need a event handler. So we'll do that. And it's going to need to receive the day that we're adding the meal to as an object. Because otherwise we wouldn't really know. That's going to be a day dot meals dot push. We'll just copy the template here for a empty meal. And then this dot model dot plan dot save. Perfect. And then now we can use that within our template. Add meal equals this dot meal. And in our components, we can now add it to this button on click this dot. It's not this. Sorry. And we actually need to use FN because we need to pass in the day as well. So now we should be able to do. Yep. And we can add meals and add lunch. And we're going to go with French toast because, you know, I really am enjoying sweets today. Cool. Finally, let's do these calories counter things here. We want to actually be able to show some derived state. So I'm going to generate Amber G some helpers to help us out with that meal calories. And then Amber G helper day calories. All right. And then in day calories, we're going to receive our day. Actually, I'll start with meal calories, meal calories. We're going to receive our meal. And I'm going to have to reuse this logic in day calories. So I'm going to export this function and then return. See, we're going to want to do let's. Recipe and servings from our meal equals meal. And then if we have a recipe, it could be null. So if so, we're going to just return zero. But if we do have a recipe, we're going to want to do math dot floor of recipe dot total calories divided by recipe dot num servings got to find out how much an individual serving is. And then we'll multiply that by the number of servings that we're having. And we do math dot floor just to make it a little bit nicer in terms of displaying. So we can use that in our component here. We'll go meal calories meal. And that looks about correct me. We had another cookie that increases the overall calories. And then for day calories, I'm actually going to just copy and paste that over here real quick. But you can see the gist of it. We import meal calories and then we have our total and we add the meal calories for each meal to our total for the day. And then I'm going to use that in our template real quick. The important part about these helpers day calories dot day is that they're all just plain JavaScript. They're using a meal with properties that have a recipe servings. The recipe relationship is just treated like a normal object, even though it is a relationship. And for the meals themselves, we can for each over it just like it's a normal JavaScript array. So all of our derived state as well, especially with this new proxy feature that I mentioned earlier is really intuitive and easy to use overall in my experience. And yeah, now we have a meal planner that can show us the calories that we're eating on our every day. And you know, I'm going to just go ahead and show off a little bit more here. Let's say that you wanted to, you know, edit those recipes. Maybe we decided, you know what, our cookies don't need that much sugar. We can just go ahead and do that. And we can see the impact that it has in real time on our meal plan, because our meal plan is represented in this very straightforward data structure that allows us to create all of this derived state based on it. And we also have all the real time capabilities of Firebase. So that to me is something that is pretty amazing. And one of my favorite features about Firestore overall. All right. So that concludes our demo. Thank you so much.