DEV Community

loading...

ToggleMap, I've Missed You

Dean Radcliffe
Updated on ・4 min read

I have a question for you about a simple device I'm sure you think you already understand - the TV (or Hulu) remote control:

What should a remote control do when you press Channel Down while it's changing?

Most people believe one of the two outcomes should happen

  • Change down 2 channels - Because: You clicked Channel Down twice, two channels should be advanced; the effect is cumulative.
  • Change down 1 channel - Because: There can only be one 'Channel Down' flow in progress at once, and you should arrive at the first channel before additional 'Channel Down' events should be recognized.

The question is - would you know exactly what code to write to implement your preferred choice of mode? And if you had to switch - how big a change would that be to your code? Another test- how easy would it be to get your choice across to another person?

A Problem of Shared Vocabulary

It turns out there are a total of 5 possible modes that can govern this behavior, and currently no library accommodates, or names them all.

  • '2 channel' mode is serial; the reason being that each channel-change is processed one after the other.
  • '1 channel' mode is mute - the future channel changes were ignored or "muted", because one was already in progress.

I looked at the RxJS library to find if there were others. RxJS has functions called operators, which in fact deal exactly with how to combine flows when they would overlap. And serial and mute correspond to two operators called concatMap and exhaustMap in RxJS. In fact RxJS has two more operators, mergeMap, and switchMap, which we can call parallel and cutoff.

Finding the Lost Operator

I sat looking at my son's favorite Halloween toy, asking it if these 4 operators represented every way a busy remote-control could handle being asked to change a new channel.

It's a goofy little noise making thing with a single button. You press the button, and it makes a spooky noise and its eyes glow.

That's when I wondered - what happens if I hit that button while it's playing the spooky music!

Want to see for yourself? Check it out on YouTube

toggleMap

If you cancel an already-executing Flow but don't start another unless no Flow was running, you are effectively toggling an Observable on/off. That's exactly what fits in the box! Let's call it toggleMap for now.

Examples!

This Loom video shows a Countdown-To-New-Year component in each of the possible modes. See how easy it is to declaratively choose a mode. One of them will surely fit your use case. How do you think the countdown timer should behave if you mash the button a few times?

Play around with it live in the CodeSandbox, it's hours of fun :)

Great - How Do I Use It?

I'll illustrate the custom hook useSmartEvents with an example of the New Year Countdown mentioned above. Each press of the Start It button begins a new Flow, counting down remaining seconds to the new year. Flows combine according to the mode parameter passed to useSmartEvents.

// usage: <YearCountdown mode="mute" />

function YearCountdown({ mode = "serial" }) {
  const [secondsRemaining, setRemaining] = useState(-1);
  const startNewCountdown = useSmartEvents(spawnCountdown, mode, setRemaining);
  return (
    <div>
      <b>{secondsRemaining}</b> seconds remaining in 2019.
      <button onClick={() => startNewCountdown()}>Start It!</button>
    </div>
  );
}

function spawnCountdown() {
    return new Observable(notify => {
    // call setInterval, notify.next(), notify.complete(), etc..
    })
}

To explain: a regular state hook defines a function setRemaining which the useSmartEvents hook will call whenever the combined Flows emit event notifications. The spawnCountdown function returns a fresh Observable of a countdown every time, while the mode parameter, accepted as a prop, shows how simple it is to declaratively set (or change!) the concurrency.

All this requires is an implementation of spawnCountdown that returns an Observable of the countdown that changes each second, and which is cancelable; see the CodeSandbox file spawnCountdown.ts for one possible implementation.

Tell Me What You Think!

This post is just meant to give you the concepts - the CodeSandbox has the goods you'll need to actually put it to work. And if you need code outside of a React context or for an event-oriented approach to structuring your app, try the Rx-Helper library.

Bring Clarity to Async

While we seem to take it as fact that Async Is Hard, perhaps the problem is that we haven't sufficiently built tools to handle the most common cases with ease. Most async cases I've seen in the wild are handleable via the 3 parameters, and 5 modes listed above.

Parallel, serial, cutoff, mute, and toggle. Start using these terms and you'll find you'll find your toolbox for async has never felt fuller.

Dean

Discussion (0)