Reducing component arguments

Learn about the Principle of Least Knowledge, and how to avoid passing too many arguments to your components.

Summary

Components should be treated like functions.

If you were writing a function that took 10 or more arguments, you would probably refactor that function to either take an object, or shift some of the responsibility from the caller to the function itself.

Similarly, components that take too many arguments should be refactored. Ember has many primitives to help us do this, and which is most appropriate depends on the use case.

In this video we use a service to shift responsibility for finding a list of countries to within an <add-friend-form> component:

// components/add-friend-form.js
export default Component.extend({

  store: Ember.inject.service(),

  countries: Ember.computed(function() {
    return this.get('store').findAll('country');
  })

});

Transcript

Hi, my name is Sam and in this video we’re going to talk about how to avoid passing unnecessary arguments to components.

One mistake that's easy to make when rendering component hierarchies in Ember is passing along too many arguments.

Components are great because they let us break up our app into many small pieces. These pieces are easier to work with, but as our app grows we'll often find ourselves with component hierarchies that are very deeply nested.

One problem that comes from having these deep hierarchies is how to get the relevant data to a component that's nested far down the tree. Often our route/controller pair is responsible for fetching and setting up the data, which means every component that's an ancestor of some deep child will have to pass the data down.

This ends up creating a situation with many components acting as middlemen for this data, passing it along to get it down to various children that need it. Let's look at an example of how this can happen.

Say we're working on a profile page for a social network. This page is concerned with a single user of the app - it displays the user's friends, newsfeed and photos.

In Ember, we use the model hook to fetch data for a route. The model hook for this route would probably look something like this:

model(params) {
  return this.store.findRecord('user', params.user_id);
}

and our server would respond with the user's details, as well as their related pictures and updates.

Now, let's say somewhere deep in this page we want to add a button that opens a form, and this form let's the user find new friends by country. This form needs to render a list of all countries so the user can select the one they want. Let's also assume that this list of countries comes from the server.

One way to solve this problem is to fetch the list of countries in the route:

// user/route.js
model(params) {
  return Ember.RSVP.hash({
    user: this.store.findRecord('user', params.user_id),
    countries: this.store.findAll('country')
  });
}

We'll then need to pass the countries down from the route to the user template, then to the <friend-list>:

<!-- user/template.hbs -->
{{friend-list
  user=model.user
  countries=model.countries}}

and again from the <friend-list> into the <add-friend-form>:

<!-- friend-list/template.hbs -->
{{#if isAddingFriend}}
  {{add-friend-form
    user=user
    countries=countries}}
{{/if}}

Now, this works, but we had to pass the countries around quite a bit just to get the form to render.

This approach actually violates an object-oriented design principle called the Principle of Least Knowledge. This principle states that each unit in a system should only have knowledge of other units that are closely related to it.

In our design above, which piece of knowledge has been scattered throughout our system?

We can see it's the fact that our <add-friend-form> component needs a countries list in order to work. This piece of knowledge has affected our route, our user template, and our <friend-list> component. If the <add-friend-form> component were even deeper, or if we wanted to reuse it elsewhere in our application, we can see that it would scatter this knowledge even further.

What's a better way to approach to this problem? It starts by recognizing that we're only using our component boundaries to separate our template markup and component logic, while we could also be using them to decouple our data flow.

The fact that the <add-friend-form> needs a list of countries, and that that list of countries never changes regardless of who renders the form, suggests that the form component itself should be responsible for obtaining that country list.

Now, Ember gives us several tools to solve this, and which one we use depends on which is most appropriate. In this case, we're going to use Ember Data's store directly.

We can use Dependency Injection to give our form component a direct reference to the store. Now, the form can fetch the countries directly using a computed property:

// components/add-friend-form.js
export default Component.extend({

  store: Ember.inject.service(),

  countries: Ember.computed(function() {
    return this.get('store').findAll('country');
  })

});

And that's it! We can now remove the countries from the route, the user template, and the <friend-list>. Our component hierarchy has become less coupled and easier to change and refactor.

One final thing to note is that we can still perform data fetching logic outside of this component, without reintroducing the coupling we saw earlier. Let's say we wanted the route to still be responsible for making the initial network request. We can have the route call store.findAll('country') in its afterModel hook; and we can even choose whether or not we want this network request to block rendering or not, based on whether we return the promise:

// user/route.js
afterModel() {
  // Block rendering
  return this.store.findAll('country');

  // Don't block rendering
  this.store.findAll('country');
}

In either case, when our <add-friend-form> component eventually renders, the list of countries will already be in Ember Data's store, and so its computed property will resolve immediately with those cached records.

This is really powerful, because we've now made a component that knows how to obtain the country list on its own, regardless of where or when it's being rendered. We could now move this form to another deeply nested part of our application without having to worry about passing the country list around.


Setting aside our specific use of Ember Data's store here, the larger lesson is that passing the same unchanging parameter deep into a component hierarchy is often a sign of coupled design. The more arguments a component needs just to be able to render, the harder it becomes to change and refactor your UI. If you ever see this happening in your own app, stop and think about how you can use Ember's primitives to reduce knowledge spillover, and write more encapsulated components.

👋 Hey there, Ember dev!

We hope you enjoyed this free video 🙂

If you like it and want to keep learning with us, we've written a free 6-lesson email course about the fundamental patterns of modern component design in Ember.

To get the first lesson now, enter your best email address below:

You can also check out more details about the course by clicking here.

Questions?

Send us a tweet:

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