Skip to content

Instantly share code, notes, and snippets.

@mlrawlings
Last active October 26, 2018 09:09
Show Gist options
  • Save mlrawlings/778ebbb701353a16c2c3f8bcf6682b7d to your computer and use it in GitHub Desktop.
Save mlrawlings/778ebbb701353a16c2c3f8bcf6682b7d to your computer and use it in GitHub Desktop.

Sugary Observables

First things first. We need a better way to manage reactive data. So there's the proposal for Observables.
Observables provide a built-in way to create an object that can pass multiple values over time and know how to clean up after themselves.

This is great, but the API is kinda... bleh. In the same way Promises are kinda bleh. There's callback functions, custom error handling, etc.

let subscription = observable.subscribe({
    next(val) { console.log("Received data: " + val) },
    error(err) { console.log("Received an error: " + err) },
    complete() { console.log("Stream complete") },
});

// some time later...
subscription.unsubscribe();

But what if... there was an async/await equivalent for Observables? observable/observe if you will:

observable function distanceTo(targetLocation) {
   const currentLocation = observe navigator.geolocation;
   return computeDistance(targetLocation, currentLocation.coords);
}

// Maths below.  The above is the important thing for this discussion.

function computeDistance(target, current) {
  const earthRadius = 6371000;
  const φ1 = toRadians(target.latitude);
  const φ2 = toRadians(current.latitude);
  const Δφ = toRadians(current.latitude - target.latitude);
  const Δλ = toRadians(current.longitude - target.longitude);
  const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
    Math.cos(φ1) * Math.cos(φ2) *
    Math.sin(Δλ/2) * Math.sin(Δλ/2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  return earthRadius * c;
}

function toRadians (angle) {
  return angle * (Math.PI / 180);
}

The above example probably didn't need to get so mathy. But it illustrates pretty well how this works.

  • The observable keyword works much like async does. It means the function will return an observable. Even if it doesn't actually observe anything.
  • The observe keyword indicates a value to be watched. It will get the observable by calling Symbol.observable. In this example, we assume that this property has been added to certain core web apis that provide watching functionality, like navigator.geolocation.
  • Any time the watched value emits a new value, the return value of the function will be recalculated.
  • Observables know how to clean up after themselves, and this is no different. When this observable is cleaned up it will automatically clean up any observables it is observing.
  • Errors are automatically propagated, can be caught with try/catch.

So that's pretty cool, we can call distanceTo(targetLocation) and get an Observable back. As I walk around, it will emit new distances. Functionally, those two lines above are somewhat equivalent to the following:

function distanceTo(targetLocation) {
  return Observable(observer => {
    const subscription = Observable.from(navigator.geolocation).subscribe({
      next(currentLocation) { 
        observer.next(computeDistance(targetLocation, currentLocation.coords));
      },
      error(err) {
        observer.error(err);
      },
      complete() {
        // the geolocation is the only thing that can cause this to update
        // so if it were to complete (it won't, but whatever), this would also be complete
        observer.complete();
      }
    });
    return () => subscription.unsubscribe();
  });
}

Where this gets even more powerful is in our ability to observe multiple things. Let's say I have a function that observes my friend's location (friendGeolocation). I could combine this with distanceFrom to watch how far we are at any given time:

observable function distanceToFriend() {
  const friendLocation = observe friendGeolocation();
  return observe distanceTo(friendLocation.coords);
}

Now whenever either one of us moves, this observable will report the new distance between us! Easily composed observables. Pretty cool.

Now. Observables are naturally async, so it would make sense that we could call other async functions from them. That's right, you can also await inside an observable function.

observable function userData() {
  const user = observe loggedInUser();
  return await fetchUserData(user.id);
}

Application to UI

observable function Counter() {
  const count = observe counter();
  return <div>{count}</div>;
}

It's like React's state hook, but better because it's not tied to a framework.

Taking it too far?

What if all scope in a observable function was watched?

observable function counter() {
  let count = 0;
  setInterval(() => count++, 1000);
  return count;
}

☝️ TODO: figure out how to clean up that interval

Marko X

Okay, so, the above is pretty cool, and we're going to bring that into the template:

<template({ count = 0 })>
  `The current count is ${count}.`
  <button on-click=(() => count++)>
    "Click Me"
  </button>
</template>

But what are these string nodes and why are they here? I'm glad you asked. Text must be represented as a string. Because javascript can go anywhere.

<template>
  const count = observe counter();
  <div>
    `${count}`
    try {
      <error-prone-component/>
    } catch(e) {
      "Yeah, that didn't work"
    }
  </div>
</template>

But inside a <template>, things work a little bit different than the outside JS world. For one, everything is memoized. And await only kinda awaits. It actually continues immediately. But the value is undefined (or an unresolved value proxy thing). When it resolves any parts of the template relying on it will be updated.

<template({ friendId })>
  const friend = await getUserData(friendId);
  <div>
    if (friend) {
     `Hello ${friend.name}!`
    } else {
      `Waiting...`
    }
  </div>
</template>

Syntax highlight, because why not? image

Actual magic: if the template does not check if the value is resolved and tries to access it when it is not, it will pause and throw a loading thing. This can be caught somewhere higher up in the tree:

<template>
  try {
    <component-that-may-need-to-load/>
  } loading {
    "Yeah, it's not ready yet."
  } catch(error) {
    "Yeah, it's failed to load."
  }
</template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment