0
votes

Here is the link for codesandbox

Hi Friends, I am learning about observable design pattern. I am trying to display bitcoin price updating but I am currently getting price as undefined. I have API being called and also created observable methods to subscribe and notify. I have an observer being created as constructor from the observable and trying to update the price state from API. But the price is showing as undefined. Can you please help me where am I going wrong ? Thank you

export function Observable() {
this.observers = []; //array of observer functions
}

Observable.prototype = {
subscribe: function (fn) {
this.observers.push(fn);
},
fire: function () {
 this.observers.forEach((fn) => {
   fn.call();
  });
 }
};

import React from "react";
import { getBitcoinPrice } from "./api";
import { Observable } from "./observable";

const CryptoPrice = () => {
const [price, setPrice] = React.useState(0);

React.useEffect(() => {
  const bitcoinObservable = new Observable();
  console.log({ bitcoinObservable });
  let livePrice = bitcoinObservable.subscribe(getBitcoinPrice);
  setPrice(livePrice);
  console.log({ livePrice });
  let newPrice = bitcoinObservable.fire();
  console.log({ newPrice });
}, [price]);

return (
  <>
    <h3>{price ? price : "undefined"}</h3>
  </>
 );
};

export default CryptoPrice;
2

2 Answers

1
votes

There were a few issues with the code. Let me start with the implementation of observable:

  1. Fire does not pass down a value to the observers! They are only notified that "something has happened", but not what exactly - so the observers would never hear the actual price.
  2. You defnitely need an unsubscribe method!

So:

export function Observable() {
  this.observers = []; //array of observer functions
}

Observable.prototype = {
  subscribe: function (fn) {
    this.observers.push(fn);
  },
  unsubscribe: function (fn) {
    const index = this.observers.findIndex(fn);
    if (index >= 0) {
      this.observers.splice(index, 1);
    }
  },
  fire: function (x) {
    this.observers.forEach((fn) => {
      fn(x);
    });
  }
};

A few things with getBitcoinPrice:

  1. const data = await response.data; is redundant. You await on promises and only axios.get(url) is a promise. After that, response.data is a real object, just use it. No harm done though, if you await on a non-promise, it just runs immediately.
  2. I saw that the data.ticker.price is a string actually, so changed it to +data.ticker.price to become a number. Again no harm done in this app, but I think this is better in the context of a bigger app.

So:

export const getBitcoinPrice = async () => {
  const url = `https://api.cryptonator.com/api/ticker/btc-usd`;
  try {
    const response = await axios.get(url);
    console.log('[getBitcoinPrice]', response.data.ticker.price);
    return +response.data.ticker.price;
  } catch (error) {
    console.log("[getBitcoinPrice] error: ", error);
  }
};

Now to the core, the CryptoPrice component. The following is one way of doing it, I am sure there are better. Anyway, I have split processing into 2 phases: one is the periodic fetching of the price and firing the observable. The other is actually observing the change and doing an effect (here updating the display). Note the use of the cleanup function of React's useEffect - never forget to release resources!

const CryptoPrice = () => {
  const [price, setPrice] = React.useState(0);
  const [bitcoinObservable] = React.useState(() => new Observable());

  // this effect is responsible for triggering the price load periodically
  React.useEffect(() => {
    const interval = setInterval(async () => {
      const x = await getBitcoinPrice();
      bitcoinObservable.fire(x);
    }, 5000);
    // the cleanup function of the effect, never forget!!!
    return () => {
      clearInterval(interval);
    };
  }, [bitcoinObservable]);

  // this effect is responsible for the subscription
  React.useEffect(() => {
    const handler = function(price) {
      console.log('[CryptoPrice]', price);
      setPrice(price);
    };
    bitcoinObservable.subscribe(handler);
    return () => {
      bitcoinObservable.unsubscribe(handler);
    };
  }, [bitcoinObservable]);

  return (
    <>
      <h3>{typeof price === 'number' ? price : "undefined"}</h3>
    </>
  );
};

Last thing, zero is falsey in Javascript, so price ? price : 'undefined' would yield undefined for the initial price of 0. Hence the type check typeof price === 'number' ? ... : ....

A couple of more things/pointers:

  • I would prefer to have the "1st phase", i.e. the creation of the "bitcoin price observable" outside of the component, probably in a service. Separation of concerns mostly.
  • If you use a state management infrastructure, such as Redux, the "service" above would be some kind of middleware.
  • A very interesting generalization of promises and the observable pattern are reactive streams. May be hard to grasp at first, but they are very powerful in the long term.
  • If you combine Redux + RxJS, checkout redux-observable.
1
votes

First of all, I'm guessing that you're intentionally building your own observable to learn somsething, instead of using a library like rxjs, so I'm gonna focus on why your current implementation doesn't work. For production usage, I would highly recommend to use a battle tested library for observables instead of building your own.

Now to your code.

1. You've mixed up the subscription and the emission (fire in your case)

Right now, your Observable will execute every function passed to subscribe if you call fire. But None of the observers will ever get the API result, because you don't pass it to fire. You should modify your observable like this:

export function Observable() {
  this.observers = []; //array of observer functions
}

Observable.prototype = {
  subscribe: function (fn) {
    this.observers.push(fn);
  },
  // add parameter value
  fire: function (value) {
    this.observers.forEach((fn) => {
      // call the observer callbacks directly with value
      fn(value);
    });
  }
};

2. You never "fire" your observable with the API result.

Instead, you've passed the Promise that returns the bitcoin price to the subscribe function. As said before, this way, no other observer could ever receive the API result.

You should modify your useEffect this way:

React.useEffect(() => {
  const bitcoinObservable = new Observable();

  bitcoinObservable.subscribe((livePrice) => {
    console.log("sub!", { livePrice });
    setPrice(livePrice);
  });

  getBitcoinPrice().then((livePrice) => {
    console.log('fire!', { livePrice });
    bitcoinObservable.fire(livePrice);
  });
}, [])

Now your code sandbox example already works fine. Of course, creating the observable and subscribing to it within the same useEffect call doesn't make much sense. But if you want to use your observable in more than one place, there is some other thing you should do:

3. Add unsubscribe logic to avoid memory leaks

React components get mounted and unmounted all the time, and the useEffect logic with them. If you don't unsubscribe from the observable, you could cause memory leaks. So you should add an unsubscribe function like this to your Observable:

Observable.prototype = {
  subscribe: function (fn) {
    this.observers.push(fn);
    // return an object with the unsubscribe function
    return {
      unsubscribe: () => {
        this.observers.splice(this.observers.indexOf(fn));
      }
    };
  },
  // ...
};

Then in your useEffect, you can unsubscribe within the effects cleanup callback:

React.useEffect(() => {
  // ...
  // save the return object of the subscribe call
  const subscription = bitcoinObservable.subscribe((livePrice) => {
    console.log("sub!", { livePrice });
    setPrice(livePrice);
  });

  // ...

  // add cleanup logic
  return () => {
    subscription.unsubscribe();
  };
}, []);

This will cleanup your subscription callbacks if the component gets destroyed. This will be necessary as soon as you use your observable in more place than one.

If you want to want to know more about observables, I recommend articles like this one: https://indepth.dev/posts/1155/build-your-own-observable-part-1-arrays