Architecture

React's Provider Pattern with Ember.js

Michael Klein

· 6 min read

React and other frameworks suggest to make use of provider components and `useContext` a lot. But is this a useful pattern when using Ember.js, and can it help to improve an Ember.js codebase, although Ember ships with a proper dependency-injection mechanism build in?

The Provider-Pattern is a way to get around the problem of prop-drilling in single-page applications and originated in the 🥁 React ecosystem . Although you might encounter the issue of prop-drilling in Ember.js codebases as well, the pattern is most useful in codebases that use frameworks that don’t have a story for dependency injection. In Ember.js though, this pattern can be used with great success to clean up code that would otherwise need to be repeated over and over again. Let’s look at prop-drilling first:

What is prop-drilling?

Prop-drilling refers to components in a component hierarchy passing down properties to their child components, although the direct children that properties are being passed to aren’t interested in the passed data, but only components further down the component hierarchy are. This is quite a mouthful, so let’s look at an example that you might encounter in the wild, where we need to determine if we want to show an edit-button in a list of blog-posts:

{{!-- posts.hbs backed by a controller --}}
<PostsList @posts={{this.model}} @user={{this.auth.currentUser}} />

{{!-- components/posts-list.hbs --}}
{{#each @posts as |post|}}
  <PostDetail @post={{post}} @user={{@user}} />
{{/each}}

{{!-- components/post-detail.hbs --}}
<div class="flex justify-between">
  <div>{{@post.title}}</div>
  <PostActions @post={{@post}} @user={{@user}}
</div>

{{!-- components/post-actions.hbs --}}
<div class="flex items-center space-x-2">
  <button type="button" {{on "click" this.transitionToPost}}>
    View Post
  </button>
  {{#if (eq @user.id @post.author.id)}}
    <button {{on "click" this.transitionToEditPost}}>
      Edit
    </button>
  {{/if}}
</div>

In the template, we are accessing the currentUser via an auth-service that makes that global application state available to the rest of the application. We access that authentication state in the controller layer and will then pass it down through the component hierarchy until we reach the post-actions-component, the component that will then use that information to determine if we should display a button that allows us to edit a post or not.

Getting rid of prop-drilling with a provider

To get around the issue of prop-drilling in this example, you make use of a provider-component for encapsulating the access to the auth-service, getting rid of the need to pass the current-user through the entire component hierarchy.

{{!-- components/post-actions.hbs --}}
<div class="flex items-center space-x-2">
  <button type="button" {{on "click" this.transitionToPost}}>
    View Post
  </button>
  <Providers::Auth as |auth|>
    {{#if (eq auth.currentUser.id @post.author.id)}}
      <button {{on "click" this.transitionToEditPost}}>
        Edit
      </button>
    {{/if}}
  <Providers::Auth>
</div>

This is an improvement over the previous example because it is arguably easier to read and also decouples the posts-list and post-detail-component from the auth-service.

With dependency-injection, do we need providers in Ember.js?

As mentioned, this is a pretty contrived example in an Ember.js codebase because you usually wouldn’t end up doing this, as you can inject the auth-service into any component directly. Sometimes you encounter this pattern in Ember.js codebases as well though, so here’s an example of how to get rid of prop-drilling without using providers and sticking to the usual Ember.js idiom of dependency-injection:

// components/post-actions.js

// The proper way of doing this in Ember.js
export default class PostActions extends Component {
  @service auth;

  get currentUser() {
    return this.auth.currentUser;
  }

  get showEditButton() {
    return this.currentUser.id === this.args.post.author.id;
  }
  
  // ...
}
{{!-- components/post-actions.hbs --}}

{{!-- ... --}}
{{#if this.showEditButton}}
  <button {{on "click" this.transitionToEditPost}}>
    Edit
  </button>    
{{/if}}

The advantages of using the provider-pattern in Ember.js

Although using dependency injection to solve the challenge at hand here is a fine approach, we can clean this up even more by using the provider-pattern. By making use of the provider-pattern, we encapsulate the access to the global application state of authentication behind an explicit public-API and can access that state even without creating backing component classes when the need to access authentication state arises anywhere in the application:

{{!-- components/navigation-bar.hbs --}}

{{!-- ... --}}

{{!-- show an avatar if logged in otherwise a link to login --}}
<Providers::Auth as |auth|>
  {{#if auth.isLoggedIn}}
    <Avatar @user={{auth.currentUser}} />
  {{else}}
    <LinkTo @route="login">
      Login
    </LinkTo>
  {{/if}}
</Providers::Auth>


{{!-- components/side-bar.hbs --}}

{{!-- ... --}}

<Providers::Auth as |auth|>
  {{#if auth.currentUser.isAdmin}}
    <LinkTo @route="administration">
      Adminisitration
    </LinkTo>
  {{/if}}
</Providers::Auth>

This might not seem like a big deal at first but in the mid- to long-term will reduce the amount of boilerplate you have to write to access authentication state in your application significantly and thus also reduce the number of bugs you have in your codebase as developers won’t need to recreate the same getters for figuring out if a user is an admin or similar over and over again.

When you create a service, create a corresponding provider

At EffectiveEmber we have embraced providers and have recommended to our clients to make use of this pattern for a long time now. Although originated in the React ecosystem to tackle the lack of a dependency-injection mechanism that you could register global app state to, it is a very helpful pattern to have in your Ember.js toolbox and will usually help you shrink the size of your Ember.js codebase significantly.

The usual recommendation we give to our clients is that whenever you create a service for global application state, you want to create a corresponding provider right away. And because you want to have the same public API available to access the service from your JavaScript files as well as from your templates, we recommend creating getters for that public API in your service directly.

That way you can make use of that public API from within JavaScript, and in your templates the same way, and you don’t need to worry keeping both variants in sync:

export default class AuthService extends Service {
  // ...
  get data() {
    return {
      // ... data you want to share from service
    }
  }

  get fns() {
    return {
       // ... functions user of service should be able to call
     }
  }
}

// when using the Auth-service from JavaScript
// /routes/posts.js

export default class PostsEditRoute extends Route {
  @service auth;

  afterModel(model) {
    if (!model.author.id === this.auth.data.currentUser.id) {
      this.transitionTo('posts.show', model);
    }
  }
}

// Auth provider that injects auth-service
// /components/providers/auth.js
export default class AuthProvider extends Component {
  @service auth;
}

{{!-- Providers::Auth --}}
{{yield (hash
  data=this.auth.data
  fns=this.auth.fns
)}}

{{!-- /components/post-detail.hbs --}}
<Providers::Auth as |auth|>
  {{#if (eq @post.author.id auth.data.currentUser.id)}}
    {{!-- ... --}}
  {{/if}}
</Providers::Auth>

Summary

Providers are especially useful when accessing global application state and can be used to DRY™ up your codebase. This pattern can also be used for other use-cases for when you want to make logic reusable that you would like to use in different places of your application over and over again. This is for an upcoming blog post, though, so stay tuned.

Providers are a useful tool to have in your client-side toolbox, even in Ember.js, where you have a dependency-injection mechanism at your fingertips that you could use to handle the issue this pattern was originally invented for to solve. At EffectiveEmber we highly recommend our clients to make use of this pattern and have seen a lot of success with clients that embrace this pattern.

When you have questions or want to share your thoughts about this post, please feel free to . We are here to help teams build ambitious applications and are always excited to learn more about the challenges you and your team face when building client-side applications.

Schedule a call

We are here to enable your team to deliver ambitious applications. Let's discuss how we can help.

European flag

© 2023 effective ember All rights reserved.