Dealing with stale client data

Work through a situation where your app falls out of sync with your server while updating a complex relationship

Transcript

In the last video, we learned how to seed Mirage’s database with some data for our post/tags many-to-many relationship. In this video, we’ll add a feature that lets our Ember app update that relationship.

Let’s take a look at our app.

Here we have our Blog posts route, and we can see the tags over here. And if we click a post, we can see the detail page where we have this tag-list up here again. And now we can click on these individual tags, and go to a tag detail page (tags/css in the url) where we see all the posts associated with that tag. And we can go back out to the list of all tags (which is the same as the route over here), and see all our tags. And we can get back to our post detail page from this list.

So, what we want to do is let the user edit the tags for a post. I’ve started working on this, as you can see here. If I click the edit pencil, a modal pops up, and right now it just says Tags, and we can show and hide the dialog. So we’ll build a list of tags here that the user can then add or remove to this post.

Alright, let’s look at the code. If I open the Post detail template, and come to the bottom, you can see that if we’re editing the tags (so if we’ve clicked that button), we render this tags-selector component. That’s right here, so let’s open this up. Here’s the template, right now it’s rendering a modal-dialog with a ui-card in it, and a simple header and empty body. And again, if we look back at the app, we can see the dialog that’s rendering the ui-card here, with the header and the empty body. And the component is empty. So let’s build out the tags selector.

We want to list out all the tags in the system, so first we need to fetch that data. We’ll do that right here in the component. We’ll import inject and inject the store, and then we’ll import an ember-concurrency task and we’ll write a new task called loadTags. In it we’ll call store.findAll and load all the tags. And we’ll yield it since this is an asynchronous function call, and return it so we can use it later. And we’ll kick this task off on init.

loadTags: task(function * () {
  return yield this.get('store').findAll('tag');
}).on(‘init’)

Now when we come to the app and open the tag selector, we see the request is being kicked off. Great. So let’s come back and list out all the tags in our dialog box. We’ll come to the card body here. And now we want to iterate over our tags using each tags as tag. But where do our tags come from? Well we can access them directly from our loadTags task, using ember concurrency’s derived state. So we can write loadTags.lastSuccessful.value, and that will give us the array of tags. And then in the loop let’s render each tag’s name.

{{#each loadTags.lastSuccessful.value as |tag|}}
  {{tag.name}}
{{/each}}

If we save this and look, we can see it’s working out. But it’s not styled. Now this app already a list component that we can use here - it’s called a {{ui-selectable-list}}. So we’ll wrap our loop in that, and selectable-list yields a contextual component. So for each item, we’ll render a list.item, and move the tag name in there.

{{#ui-selectable-list as |list|}}
  {{#each loadTags.lastSuccessful.value as |tag|}}
    {{#list.item}}
      {{tag.name}}
    {{/list.item}}
  {{/each}}
{{/ui-selectable-list}}

Let’s check it out. Nice, it looks great. Ok I like keeping my templates as readable as possible, so I’m just going to replace this load.lastSuccessful.value bit with a tags varaiable. And we’ll import readOnly and use that here.

tags: readOnly('loadTags.lastSuccessful.value’)

Ok, everything is still working.

Alright, next, our selectable list items expose a selected property, and if its true we’ll see a checkbox in our list. Let’s take a look. And there we see the list and the X’s, all styled and rendering just like we saw in the mockup. Aren’t these UI components great? I really love building out features using ui components that already exist, because they take care of styling my app, and let me focus on just the business functionality of this current feature. So, if you haven’t given these presenter or ui components a shot, I’d definitely recommend trying them out.

So, looking at our app we only want selected to be true for tags that are currently associated with our post. We can use the contains helper from ember-composable-helpers to say this will be true when tag is contained in post.tags.

And now we see on our post that has javascript and opinion tags, if we open the selector, we see JavaScript and Opinion are checked and CSS is not. Great.

Ok, now for the functionality. When we click on these list items we want to toggle the association. Let’s come to our code, and right here on this list.item we can pass in an onClick handler, and let’s have this perform a new ember concurrency task we’ll call toggleTag, and we’ll pass in the tag from the current loop.

{{#list.item
  selected=(contains tag post.tags)
  onClick=(perform toggleTag tag)}}
  {{tag.name}}
{{/list.item}}

Now we’ll come to the component and define toggleTag, it’ll be a task with one parameter, the tag…

toggleTag: task(function * (tag) {
  let post = this.get('post');
  let tags = post.get('tags');

  if (tags.includes(tag)) {
    tags.removeObject(tag);
  } else {
    tags.addObject(tag);
  }

  yield post.save();
})

So what we’re doing is updating the post.tags relationship directly on the post, and then calling .save() on the post. If we click on it - it seems like it works! And if we remove Javascript, and then come to the Javascript tag, we don’t see our Top 10 Libraries post here. So it looks like everything is working correctly.

We can verify this by inspecting Mirage’s database. Let’s go back to this Top 10 post and reload, and in our console type server.db.dump(), and then open the first post, and we see tagIds are 1 and 3. And we also can look at the tags, and we see that the Javascript tag has post 1, and the Opinion tag has post 1.

Now let’s remove the JavaScript tag from the post, and look at the new snapshot of the database. Post 1’s tagIds are now just 3, and the Javascript’s postIds are empty. So the foreign keys are all correct.

Alright, let’s look at our Mirage config and see how this is working. So right now we have this this.resource(‘posts’) declaration, and this sets up all of mirage’s default shorthands for the different verbs to POST - so, get to /posts, get to /posts/:id, patch to /posts/:id and so on.

Now, the patch request shorthand is what we’re using here, since we’re updating a post that already exists. And by default, this shorthand will take all the attributes and relationships that our Ember app sends over in the request, and update our model. And as we saw, it works for our simple case.

Now, I want to show you another part of the app that I had uncommented here. I’ll come back to my post template, and uncomment it here. And as you can see back in the app, we have an activity feed, that shows us whenever a tag was added or removed from the post.

But there’s just one problem - our tag-selector feature we just built doesn’t update the activities! This is because the default Mirage shorthand is just updating the post’s attributes and relationships. In the real app, though, our server actually creates new activities whenever this relationship is changed. So, we’ll need to write a custom route handler to help simulate this change, so we can make sure our app is working correctly.

Alright, let’s come back to Mirage’s config, and we’ll define

this.patch('/posts/:id', (schema, request) => {
});

Now because we’re defining this after our resource declaration, our custom handler will overwrite the default shorthand.

So first, let’s start by reproducing what the shorthand is doing for us, and then we’ll tackle the activity creation.

So first, we want to find the post by id. And then, we want to get all the attrs from the request payload. Now we could use JSON parse on the request.requestBody string, but Mirage has a helper method called normalizedRequestAttrs(). To use this though, we need to make sure to switch this from a fat arrow function to a regular function, since it uses the this context of the handler.

Let’s throw a debugger in here and take a look at what normalizedRequestAttrs gives us. We can see that it has all our attrs, even including our foreign keys, all ready to go to update our model. If we look at what our Ember app sent us, we can see this full json:api document is what came with the request payload. So, normalizedRequestAttrs is just taking this document and normalizing it, to make it easy to work with with Mirage’s ORM methods.

Let’s come back to our route handler, and now that we have the attrs and our post, we can return post.update(attrs):

this.patch('/posts/:id', function(schema, request) {
  let post = schema.posts.find(request.params.id);
  let attrs = this.normalizedRequestAttrs();

  post.update(attrs);

  return post;
});

And if we try it in our app, everything’s working. And we can verify here in the console, server.db.dump().posts[0] and look at tagIds, and its indeed empty.

Alright, so this route handler is basically the shorthand. And I just want to point out in case you haven’t written a lot of custom route handlers, that just by returning a model or collection here from the end of our custom handler, Mirage will recognize it and then be able to properly serialize it. So the rule with these handlers is basically, do whatever work you need to do, and then return a model or collection, and it will get properly serialized.

Ok, so we’ve recreated the shorthand, but we want to do a bit more here, to simulate our new activity creation.

Let’s start by making a simple activity. In our handler we’ll call schema.activities.create(). If we pop open our Post model, we’ll see it has a hasMany relationship to activities, so then in our handler after updating our post, we can call post.activities.add() and pass in our new activity. This will associate the new activity with our post. We’ll then save the post to persist this new assocaition, and return our post.

this.patch('/posts/:id', function(schema, request) {
  let post = schema.posts.find(request.params.id);
  let attrs = this.normalizedRequestAttrs();
  let activity = schema.activities.create();

  post.update(attrs);
  post.activities.add(activity);
  post.save();

  return post;
});

Alright, let’s come look at our app. We can see from mirage’s db that our post starts out with having 2 activities, and after we change a tag, it has 3. But the new activity is not showing up in our list.

And that’s because if we look at the PATCH request, we can see it comes back with just the attributes - it doesn’t include any relationship information by default. And this is how many backends will work too, or they might return something with links pointing to those relationships.

So, I want to pause here for a moment, because this issue comes up regularly in Ember apps. Our server is in a good state and its data is updated and correct, but part of our client has become stale. So, let me ask you, how might you tackle this, if you were coding this feature right now on your own? Feel free to stop the video and think about it for a minute on your own or with your team.

Now, there’s a couple of different ways we could deal with this. First, since we know selecting a tag can “dirty” the post.activities relationship, we could add some code to the end of our toggleTag task to just reload that relationship. And we could do that using either the References API, links, or through an async relationship.

Now, since our server is in a good state, we could also just refresh the route’s model, either after a user has toggled a task, or we could even have our route poll regularly to check for new data. In this case our activity feed would update only when a new poll went out, but we could make it frequent enough so the user experience was good.

But there’s a third approach we can use that’s quite nifty, and it involves having the backend side-load the new data alongside the main payload when it responds to the request. In our case, this would mean when we send a PATCH request to posts/1, our backend would respond with the post and also any new activities it created. This way we can avoid adding new asynchronous code or putting our app into yet another state - because the new data shows up within this single request-response cycle.

Now most backend frameworks have an easy way to configure this, so Mirage also makes this pretty straightforward. Mirage handles this in the serializer layer using default includes. So first, let’s open up the console and generate a serializer for our post with

ember g mirage-serializer post

Now all we need to do is write include, and this takes an array of associations, and we’ll add activities. And now whenever our Mirage server responds with a post, it will side-load that posts activities by default.

Let’s come back to our app and try this out. Alright, we see the list updating! And in the PATCH request, we can see in the response we have 3 activities being included. So now our Ember app knows about the new activity our server has created.

Alright, the list is updating but the activity has no text and is also out of order. Let’s come back to the route handler, and we’ll give it some text, and activities also have a createdAt which we’ll set to a new Date at the current time.

let activity = schema.activities.create({ text: ‘A tag was changed’, createdAt: new Date() });

Now in the app, we see the text, and the new activity was appended to the top of the list.

Ok, we’re almost there! We just need the activity’s text to be correct. We want it to say, whether a tag was added or removed. Let’s create a new variable called activityText. We’ll also grab the currentTags from the post before we have a chance to update it, and the new tags as well. We can get the new tags by calling schema.tags.find, and then passing in the tagIds from our payload, which we can get from attrs.tagIds.

Now we can write some logic to get the correct text for our new activity. We know if theres more new tags than current tags, the user added a tag, and if there are more current tags than new tags, the user removed a tag. So let’s write this out.

If newtags.length is greater than currenttags.length, that means we’ve added a tag. So let’s find the addedTag - we can get it by getting our newTags and filtering out tags that aren’t in the currentTags collection. So we’ll call filter. And this is a filtered Mirage collection, but we know it only has one model in it, so we can call .models[0] here. .models is how you access the underlying array from a Mirage collection.

And now we have the tag, and we want the activity text to say something like “The JavaScript tag was added”, but we’ll interpolate the name using the added tag.

Now let’s take the other case. If the currentTags are longer than the newTags, we’ll do the same thing but flip it - we want the tag that’s in currentTags but not in newTags. And we’ll change the text.

And now we have our activity text, and we can pass it into our new activity on creation, and let’s save this and try it out!

this.patch('/posts/:id', function(schema, request) {
  let post = schema.posts.find(request.params.id);
  let attrs = this.normalizedRequestAttrs();
  let activityText;
  let currentTags = post.tags;
  let newTags = schema.tags.find(attrs.tagIds);

  if (newTags.length > currentTags.length) {
    let addedTag = newTags.filter(tag => !currentTags.includes(tag)).models[0];
    activityText = `The ${addedTag.name} tag was added`;

  } else if (currentTags.length > newTags.length) {
    let removedTag = currentTags.filter(tag => !newTags.includes(tag)).models[0];
    activityText = `The ${removedTag.name} tag was removed`;
  }

  let activity = schema.activities.create({
    text: activityText,
    createdAt: new Date()
  });

  post.update(attrs);
  post.activities.add(activity);
  post.save();

  return post;
});

And the activity feed is working great. And if we remove javascript, and go to tags, we don’t see our post in this list, and all the data is as we expect.

Looking at our route handler, it’s clear that we coded this for our single use case - for example this logic wouldn’t work if we added a tag and removed a tag in a single request - but hopefully this example gave you a better idea on how to work with many to many relationships, and how to know where to look when things go wrong. It’s extremely useful to be able to write your own handlers that simulate complicated server scenarios when the need arises, so you have a frontend that’s rock-solid and tested before you ship it off to production.

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.