Rewrite React Component to Ember

Hello everyone, nice to meet you all.

I am new to ember and I am trying to rewrite a react component to ember for learning porpoises. This is a resume of the component: ArtistPage.tsx

export default function ArtistPage(): JSX.Element {
  // ......
  const [wikipediaExtract, setWikipediaExtract] = React.useState<
    WikipediaExtract
  >();

  React.useEffect(() => {
    async function fetchWikipediaExtract() {
      try {
        const response = await fetch(
          `https://musicbrainz.org/artist/${artistMBID}/wikipedia-extract`
        );
        const body = await response.json();
        if (!response.ok) {
          throw body?.message ?? response.statusText;
        }
        setWikipediaExtract(body.wikipediaExtract);
      } catch (error) {
        toast.error(error);
      }
    }
    fetchReviews();
    fetchWikipediaExtract();
  }, [artistMBID]);
  return (
    {wikipediaExtract && (
      <div className="wikipedia-extract">
        <div
          className="content"
          // eslint-disable-next-line react/no-danger
          dangerouslySetInnerHTML={{
            __html: sanitize(wikipediaExtract.content),
          }}
        />
        <a
          className="btn btn-link pull-right"
          href={wikipediaExtract.url}
          target="_blank"
          rel="noopener noreferrer"
        >
          Read on Wikipedia…
        </a>
      </div>
    )}
  );
}

This is my current ember component artist.js

  export default class Artists extends DiscourseRoute{
  async model(params) {
    // Fetch the main artist data
    const artistData = await this.store.find('artist', params.id);

    // URLs for additional data
    const wikipediaURL = `https://musicbrainz.org/artist/${params.id}/wikipedia-extract`;

    // Fetch reviews and Wikipezzzdia extract concurrently
    const wikipediaResponse = await fetch(wikipediaURL);

    // Process responses
    const wikipediaJson = await wikipediaResponse.json();

    // Check for fetch errors
    if (!wikipediaResponse.ok) throw new Error(wikipediaJson?.message || wikipediaResponse.statusText);

    // Structure the model with all necessary data
    return RSVP.hash({
      artist: artistData,
      wikipediaExtract: wikipediaJson.wikipediaExtract
    });
  }
}

with a synchonous aproach, I am fetching artist, which is the main model data and the wikipediaExtract in the model hook

The cons with this approach is that I want to be able to render the template even if the wikipediaExtract call fails.

For what I’ve researched, there are a couple of possible solutions:

1. Extend model:

export default DS.Model.extend({
  wikipediaExtract: Ember.computed(function() {
    const wikipediaURL = `https://musicbrainz.org/artist/${params.id}/wikipedia-extract`;

    const wikipediaResponse = await fetch(wikipediaURL);

    const wikipediaJson await wikipediaResponse.json();

    return { wikipediaExtract: wikipediaJson.wikipediaExtract }

  }
});
  1. With controllers

artist-controller.js

export default class ArtistsController extends Controller {
  @service artistData;

  @tracked wikipediaExtract;
  @tracked isLoadingWikipediaExtract = false;

  @action
  async loadWikipediaExtract() {
    const artistId = this.model.id;
    this.isLoadingWikipediaExtract = true;

    try {
      const wikipediaURL = `https://musicbrainz.org/artist/${params.id}/wikipedia-extract`;

      const wikipediaResponse = await fetch(wikipediaURL);

      const wikipediaJson await wikipediaResponse.json();
      this.wikipediaExtract = wikipediaJson.wikipediaExtract;
    } catch (error) {
      console.error('Error fetching additional data:', error);
    } finally {
      this.isLoadingWikipediaExtract = false;
    }
  }
}

3. afterModel hook

similar to the controller solution but using the afterModel hook

I understand that each solutions comes with i’ts own pros and cons and would like to know your take, considering the usecase. Thanks in advance

PD: for reference, here is the complete react component source code: ArtistPage

1 Like

First off, welcome!!! I used to do React as well (I also got really in to hooks when they were introduced, and was excited about building a good framework with React for my employer at the time!)


But, to your code / questions.

What version of ember-source are you using? I’m noticing a mix of some old apis along with some modern ones.

The cons with this approach is that I want to be able to render the template even if the wikipediaExtract call fails.

This is correct – routes are for minimally required data and when that route-mode-hook errors you get an error page – so if you have optional data, you maybe don’t need a route.

  1. Extend model

function () + await is a syntax error which is easily solved with an async keyword – However! computed properties can’t be async.

There are a couple ways to have async-reactive data tho (these all use a library, but you can minimally re-create this yourself in your app)

Conceptually, all of these do something like this:

class Demo extends Component {
  @cached
  get request() {
    let promise = fetch(this.args.url);

   return makeReactivePromise(promise);
  }
}

this works because @cached is not just for caching expensive computations, but also for maintaining referential integrity, so if args.url doesn’t change, multiple calls to this.request do not repeatedly call fetch – at the same time, when url changes, fetch is called again.

However, you don’t get cancellation or a shared request cache.

Depending on which API you choose, you’d have different ways of handling success vs error, for example, if we’re “rolling our own”, you might have something like this when using the above code:

{{#if this.request.isPending}}
   ... loading ...
{{else if this.request.isError}}
   oh no!
{{else if this.request.isSuccess}}
  do something with data
{{/if}}
  1. with controllers – I would avoid controllers when possible – they are handy for some query param management, but general data management and state management is awkward – components are really good at this, and more conceptually cohesive as components are not singletons like controllers are.

Lemme know if you have questions!

2 Likes

Thanks for the warm welcome and for taking the time to answer with such detail.

What version of ember-source are you using? I’m noticing a mix of some old apis along with some modern ones.

I am running version: 5.5.0 but I took the code examples from other answers here and there and adapted them a little to what I need, they were only illustrative :laughing:

thanks for the resources on async-reactive data!! super handy

Your code extends a component, is there a reason why you choose this instead of the model? I read here https://stackoverflow.com/a/42102097/4616973 that it was better if components have no connection to the data source.

1 Like

I don’t like adding logic to models – I consider them to be strictly concern with the API, and will use utility classes / wrappers / etc if I need custom behavior – doing this makes it much easier to deal with upgrades over time (composition over configuration, etc).

The class doesn’t have to be a component – it could even be a vanilla class {} – there isn’t really anything special about ember reactivity, except that for cancellation (specifically), you need to wire up the ownership graph (this.owner or getOwner(this), usually) – and for that I made a helper to make that super easy as well – here: link | reactiveweb

1 Like

Also (meaning in addition to @NullVoxPopuli’s great advice) I wouldn’t necessarily take this whole SO answer too seriously. There are multiple schools of thought and it really depends on the use case and it also depends on how thoughtful you are with your choices, but there’s not necessarily anything wrong with loading data in components.

The router is built to help make all those choices for you and so it’s easier to stay in safe waters if you’re using the router to load your data, but it also doesn’t fit some use cases super well (like yours).

2 Likes

Hi guys! Thanks again for taking the time to respond, I really appreciate it.

Found some time today to take a deeper look at the solutions.

I ended up trying this approach I saw on the discourse code base

export default class Artists extends DiscourseRoute{
  async model(params) {
    return this.store.find('artist', params.id);
  }

  afterModel(model, transition) {
    const reviewsURL = `https://critiquebrainz.org/ws/1/review/?limit=5&entity_id=${model.id}&entity_type=artist`;
    const wikipediaURL = `https://musicbrainz.org/artist/${model.id}/wikipedia-extract`;

    const reviewsData = fetch(reviewsURL).then(
      (result) => {
        this.reviewsData = result.json();
      }
    );

    const wikipediaData = fetch(wikipediaURL)
      .then(response => response.json())
      .then(data => this.wikipediaData = data.wikipediaExtract);

    const promises = {
      reviewsData,
      wikipediaData,
    };

    return hash(promises);
  }

  setupController(controller, model) {
    controller.set("model", model);
    controller.set("reviewsData", this.reviewsData);
    controller.set("wikipediaData", this.wikipediaData);
  }
}
export default class ArtistController extends Controller {
  @tracked wikipediaData;
  @tracked reviewsData;

  @cached
  get request() {
    let promise = fetch(this.args.url);

    return getPromiseState(promise);
  }

and in the template;

    {{#if this.wikipediaData}}
      <div class="wikipedia-extract">
        <div class="content">{{html-safe this.wikipediaData.content}}</div>
        <a class="btn btn-link pull-right" href="{{this.wikipediaData.url}}" target="_blank" rel="noopener noreferrer">
          Read on Wikipedia…
        </a>
      </div>
    {{/if}}

I will give it a try to some of the libraries later but in the meantime, I was wondering about a couple of things:

does this mean that computed properties can’t be async but tracked properties yes?

what do you mean by cancellations and shared request cache? I checked out the url about linking but did not quite understand it

I were to create a vanilla class to encapsulate the function that returns the reactive Promise, how could I be able to use it in my templates?

PD: thanks in advance and sorry if the questions seem silly, I am a backend guy trying to find the way in the js world

1 Like

nay, but computed properties (or more preferred: getters) can return a promise (however, note that you’d want to wrap that promise in some reactive handling, such as ember-async-data (or your own wrapper if you want different behavior) – which would allow you to do something like

{{#if this.wrappedPromise.isLoading}}
  ...
{{else}}
  not loading!
{{/if}}

as promises don’t natively have properties that we can use for rendering (or really any properties we can react to at all!)

these tend to be more advanced features of request / data management that ember-data / wrap-drive are trying make easy – but documentation there is very young. I wouldn’t worry about it quite yet (unless you have requirements to worry about it, ofc)

this one? link | reactiveweb this is about wiring up vanilla classes so that you use service injection and destructors outside of framework objects.

Like this:

import Component from '@glimmer/component';
import { link } from 'reactiveweb/link';
import { cached, tracked } from '@glimmer/tracking';
import { TrackedAsyncData } from 'ember-async-data';

class Demo extends Component {
  <template>
      {{#if this.myInstance.isLoading}}
          ...
      {{/if}}

      {{#if this.myInstance.value}}
          data here {{this.myInstance.value}}
      {{/if}}
  </template>

 @tracked foo = 'bar';

  @link myInstance = new MyClass({
     foo: () => this.args.foo,
     bar: () => this.bar,
  });
}

class MyClass {
  constructor(lazyArgs) {
    this.#foo = lazyArgs.foo;
    this.#bar = lazyArgs.bar;
  }

@cached
  get request() {
    let promise = fetch(`https://${this.#bar()}/some-path/${this.#foo()})
    return new TrackedAsyncData(promise);
  }

  get isLoading() {
    return this.request.isPending;
  }

  get value() {
    return this.isLoading ? null : this.request.value;
  }
}

None of this is silly! There is a lot of context to learn in frontend! These are good questions :tada:

thanks you very much for your answers, super helpful!

I’ll keep playing around with ember so I guess I’ll see you around :grin:

1 Like