Architecture

Resources change everything

Michael Klein

· 5 min read

Resources are a new layer of abstraction that you can use in your Ember.js apps today and will be a part of Polaris edition. What are resources, and why do we think they change everything for Ember.js development?

The idea of resources , or usables when the concept was introduced, has been discussed in the Ember.js ecosystem for quite some time now. With the `ember-resources` add-on , you can try out resources in your applications today. In this blog post, we want to help you understand why resources, in our opinion, will be a big game changer in Ember.js development and what makes them so cool to use:

Resources are yet another stateful instance of a thing in your system

Ember.js ships with multiple abstractions that are stateful in nature. Services, Components, Helpers (class based that is) and even Routes and Controllers hold state.

But the only level of abstraction that most people used, to write reusable code that you could use in your application over and over again before Resources are Components and Helpers. We’ll leave helpers out for now because the difference between them and Components gets blurry quickly, and it probably is easier to follow a train of thought that makes Compoonents the foundational building block of your application. This is how all other frameworks like, React, Vue or Svelte think of components as well, the smallest possible building block that you create and then assemble to a bigger whole, much like Lego bricks.

Old world - You want to make something reusable, create a component

Let’s look at an example use-case where you as the great developer you are wanted to encapsulate the “something asynchronous is happening in your system”-use-case once and for all. Kudos to you because if you want to do something like that you are probably in the top 10% of frontend programmers in today’s world 👏, but how would you use something like that in Ember.js as a component abstraction? We are assuming you use ember-template-imports so that you can make use of single-file-components:

import Async from './async';
import { on } from '@ember/modifier;'

export default class DoSomething extends Component {
  async doSomethingAsync() {
    // ...
  }
  
  <template>
    <Async @do={{this.doSomethingAsync}} as |async|>
      <button
        type="button"
        disabled={{async.isDoDisabled}}
        {{on "click" async.do}}
      >
        Do something async
      </button
    </Async>
  <template>
}

The inner-workings of Async don’t really matter that much, but we’ll assume that it is well-made and does what it is supposed to do. In an ideal world Async's behavior would be driven by a statechart , of course, but I digress 😉.

Brave new world - create a resource instead

When using Resources the template changes slightly:

import { on } from '@ember/modifier';
import { Component } from '@glimmer/component';
import { Async } from '../resources/async';

export default class DoSomething extends Component {
  async doSomethingAsync() {
    // ...
  }

  async = Async.from(this, () => {
    const { doSomethingAsync } = this;
    
    return {
      do: doSomethingAsync
    }
  })

  <template>
    <button
      type="button"
      disabled={{this.async.isDoDisabled}}
      {{on "click" this.async.do}}
    >
      Do something async
    </button>

  </template>
}

On the surface, this doesn’t make that much of a difference, BUT it decouples you from the templating layer and prevents you from having to nest providers deeply. Concept wise, the Async-component does the same but in a less flexible way, plus it creates the need for a lot of nesting in your template - which gets weird quickly. Here’s an example where we are combining a Form-component that encapsulates a form-handling-abstraction and the Async-component we discussed previously:

// ...
export default class MoreComplexComponent extends Component {
  // ...
  <template>
    <Form
      @object={{this.teamFormObject}}
      @onSubmit={{this.submitTeam}} as |form|
    >
      <Form::Ui::Form {{on "submit" form.submit}}>
        {{!-- ... --}}
        <Async @do={{this.refreshPlayers}} as |async|>
          <button
            type="button"
            disabled={{async.isDoDisabled}}
            {{on "click" async.do}}
          >
            Refresh players
          </button
        </Async>
        <Form::Ui::Cta disabled={{form.isDisabled}}>
          Submit
        </Form::Ui::Cta>
      </Form::Ui::Form>
    </Form>
  <template>
}

This isn’t too bad in this example, but you can see how this can get out of hand in more complex use-cases. Resources let you pull the reusable parts of your application that encapsulate behavior into the JavaScript layer of your app, a small difference but this helps to clean up your templates:

// ...
export default class MoreComplexComponent extends Component {
  // ...
  refreshPlayers = Async.from(this, () => {
    // ...
  })

  form = Form.from(this, () => {
    // ...
  })

  <template>
    <Form::Ui::Form {{on "submit" this.form.submit}}>
      {{!-- ... --}}
      <button
        type="button"
        disabled={{this.refreshPlayers.isDoDisabled}}
        {{on "click" this.refreshPlayers.do}}
      >
        Refresh players
      </button
      <Form::Ui::Cta disabled={{this.form.isDisabled}}>
        Submit
      </Form::Ui::Cta>
    </Form::Ui::Form>
  <template>
}

Is this significantly better than the version that makes use of reusable Components that encapsulate behavior? Quite honestly, it depends, and it probably won’t make or break your application. But being able to use Resources in JavaScript as well as in the templating layer is powerful. You could for example create a Watcher-resource that allows you to subscribe to changes via a web-socket or long-polling, that you can use in your routes or declaratively in the template layer:

// in the route layer
export default PlayersIndexRoute extends Route {
  constructor() {
    super(...arguments);

    this.watchedPlayers = Watcher.from(this, () => {
      // ...
    })
  }

  // ...

  activate() {
    // explicitly start watching - route is a singleton
    this.watchedPlayers.start();
  }

  deactivate() {
    // explicitly stop watching - route is a singleton
    this.watchedPlayers.stop();
  }
}

// in the component layer
export default class PlayersListComponent extends Component {
  // will be cleaned up automatically when component unrenders
  watchedPlayers = Watcher.from(this, () => {
    // ...
  })

  constructor() {
    super(...arguments);

    this.watchedPlayers.start();
  }

  <template>
    {{#if this.watchedPlayers.isLoaded}}
      {{#each this.watchedPlayers.data as |player|}}
        <PlayerCard @player={{player}} />
      {{/each}}
    {{/if}}
  <template>
}

This truly is a much better situation than we had before in Ember.js. With Resources we now can create reusable building blocks that can be used from the template layer and JavaScript. This allows you to clean up legacy patterns easier, as you don’t have to come up with two abstractions that work in two different layers of abstraction, Components or Routes et al.

Bonus: Resources are helpers and you get a provider for free

Internally, resources are implemented based on the `setHelperManager` - and `invokeHelper` -API. This means that technically Resources are helpers. To let your application know about a Resource that you directly want to invoke as a helper from the template layer (for whatever reason you intend to do that) you just need to export it from the helpers-directory of your application:

// helpers/form
export { default } from '../resources/form';

You can then use your Resource like you would use any other helper:

{{#let (form
  object=this.teamFormObject
  onSubmit=this.submitTeam)
  as |form|
}}
  {{!-- ... --}}
{{/let}}

Bonus: Resources can use other resources

Resources can use other resources. This might not sound that interesting but especially for us at EffectiveEmber, this is an exciting value proposition. Why? Because the ember-statecharts-add-on we use to model application behavior explicitly in the applications we write, provides its useMachine-API as a Resource. This means you can drive your Resources’ behavior with a statechart 🤯 🥳

// resources/async
export default class Async extends Resource {
  // ...

 statechart = useMachine(this, () => {
    return {
      machine: asyncMachine.withConfig({
        actions: {
          onError,
          onSuccess,
        },
        services: {
          onSubmit
        }
      })
    }
  });

  submit = () => {
    this.statechart.send('SUBMIT');
  }

  // ...
}

We wrote a blog-post about the benefits of modeling your application behavior explicitly via statecharts. You can also have a look at the ember-statecharts documentation to learn more about the add-on.

Summary

Resources aren’t really a complete new shift in paradigm of how you would break your applications down into smaller pieces. You are still thinking about what are the the smallest possible units of behavior you want to make reusable, but Resources make for such a better story than just Components that are bound to the rendering context and nesting in your template files.

To say we are exciting for Resources in Ember.js would be an understatement. The concept of Resources allows you to extract parts of your application that you would like to reuse with little coupling to the Ember.js framework itself. Mostly they are just JavaScript. Initiatives like Starbeam.js try to bring the concept of Resources to other frameworks as well. This might lead into a world where we as app developers spend most of our time developing framework independent abstractions that we can then use from whatever JavaScript framework we decided to choose for the project at hand. Exciting times, indeed 🚀.

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.