3
\$\begingroup\$

I work on a small Web Component that converts an amount of money to another currency. For that, I use an API that returns the exchange rates.

I want to use the component multiple times on a page and thought that it would be a waste of traffic if every component fetches the same exchange rates. So I wrote some logic that only the first handled component fetches it, stores the results in a window variable. The other components notice that some already loads the data and store a callback in that window variable. When the fetching is done all callbacks will be resolved.

This works great but I wonder if this is a bad practice or whether this is even unnecessary because maybe it is cached etc.

Code (the described part is in the "connectedCallback" function):

const template = document.createElement('template');
template.innerHTML = `
  <style>
    .curr-shell {
      color: #262626;
      background: #F1F5F9;
      border-radius: 16px;
      padding: 4px 8px;
      font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
      box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
    }
  </style>
  <span class="curr-shell"></span>`;

class CurrencyConverter extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }

  async fetchApi() {
    return new Promise((resolve, reject) => {
      fetch(`https://api.exchangeratesapi.io/latest?base=${this.baseCurrency}`)
        .then((response) => response.json())
        .then((data) => resolve(data))
        .catch((err) => reject(err));
    });
  }

  renderData() {
    const browserLocale = navigator.language;
    const originFormat = new Intl.NumberFormat(browserLocale, {
      style: 'currency',
      currency: this.baseCurrency,
    });
    const conversionFormat = new Intl.NumberFormat(browserLocale, {
      style: 'currency',
      currency: this.conversionCurrency,
    });

    const originAmount = originFormat.format(this.value);

    this.convertedAmount =
      this.value *
      window.rates[this.baseCurrency].data.rates[this.conversionCurrency];

    const conversionAmount = conversionFormat.format(this.convertedAmount);

    this.shadowRoot.querySelector(
      '.curr-shell'
    ).innerText = `${originAmount} | ${conversionAmount}`;
  }

  async connectedCallback() {
    this.baseCurrency = this.getAttribute('base-currency');
    this.value = parseFloat(this.getAttribute('value'));
    this.conversionCurrency = this.getAttribute('conversion-currency');

    if (!window.rates) window.rates = {};

    if (!window.rates[this.baseCurrency]) {
      // first one -> fetch api
      window.rates[this.baseCurrency] = {
        status: 'fetching',
        data: null,
        callbacks: [],
      };

      window.rates[this.baseCurrency].data = await this.fetchApi();
      window.rates[this.baseCurrency].status = 'loaded';
      this.renderData();
      // resolve all callbacks from the waiting ones
      window.rates[this.baseCurrency].callbacks.forEach((cb) => cb());
    } else if (window.rates[this.baseCurrency].status === 'fetching') {
      // currently some else is fetching -> add callback to be called when done
      window.rates[this.baseCurrency].callbacks.push(() => this.renderData());
    } else {
      // all data loaded
      this.renderData();
    }
  }
}

window.customElements.define('currency-converter', CurrencyConverter);

Demo: https://phartenfeller.github.io/currency-converter-wc/demo/

\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

From a super short review;

  • I would attach the style tag to the head element, especially if you can have this element several times on the page (and ensure that you only add it once)

  • When all a fat arrow function does is call an another function with the provided parameter(s), then you might as well

      .then(resolve)
      .catch(reject);
    

    instead of

      .then((data) => resolve(data))
      .catch((err) => reject(err));
    
  • This will not work in a rainy day scenario;

     window.rates[this.baseCurrency].data.rates[this.conversionCurrency];
    

    Consider how you would handle this if window.rates[this.baseCurrency] is missing.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.