How an Interview Convinced Me to Use π—₯π˜…π‘“π‘₯ and RxJS for Effect Management in React

deanius

Dean Radcliffe

Posted on May 20, 2022

How an Interview Convinced Me to Use π—₯π˜…π‘“π‘₯ and RxJS for Effect Management in React

Edit: Here is part 2 of this series.

Note: Some images refer to the library's old name "Omnibus-RxJS" and have not been updated to π—₯π˜…π‘“π‘₯.

An Interview Like No Other

It was a front-end React UI developer interview which began like any other. But unlike others, by the end of it I'd learned enough to change my whole approach to data-fetching, or maybe to async programming in general.

I was the interviewer for a 60 minutes live-coding interview. The goal was to build a simple React GUI that could fetch a random GIF. We assumed any candidate who reached this round could do async data-fetching in React. But we wanted to learn how a candidate thought about front-end problems in general. We were looking for some insight that we didn't already have. And in this candidateβ€” call them Chris - we found it :)

I handed Chris the paper with the requirements for their live-coding interview.

Getting Acquainted

The requirements contained an image of what was to be built in the interview.

empty template

Implement a React UI that loads a random GIF upon the click of a button Respond to and display any errors that occur Implement a cancel button

Chris asked a few questions:

"Does it have to be cats, or will any GIF do?"

"Ha!" I said. "You can choose any GIF you want, but we've provided a function to get cats."

const fetchRandomGif = () =>
  fetch("https://api.thecatapi.com/v1/images/search", {
    headers: { "x-api-key": "blah-blah" },
  })
    .then((res) => res.json())
    .then((data) => data[0].url);
Enter fullscreen mode Exit fullscreen mode

"Can I use any libraries?" was Chris's next question. I replied: "Do you think one is needed for this app?"

"Well, we need a cancel button... And I find Observables make for cleaner, less error-prone code around cancelation."

This took me by surprise. I only knew of one cancelation tool, AbortControllers, and Chris pinpointed my frustrations with them - that they're easy to forget, don't compose well, and obscure the logic of the happy path.

I said, "yes you can use a library, but you must be able to explain what it is doing, and justify its contribution to the bundle size."

Chris chimed up with, "Sounds goodβ€” shall we get to work then?"


π—₯π˜…π‘“π‘₯β€”An Odd Choice of Library

Chris started by creating a new file, naming it gifService.ts. I gently inquired why they made a separate file instead of coding into the provided React component.

"React is the view layer, this is- well, a service layer. Easier to test with no React dependencies. Can I proceed?"

"Go for it," I said. Chris wrote the following as though from memory, and in TypeScript, but I'll post just the JS here.

import { Bus } from "@rxfx/bus";
import { createService } from "@rxfx/service";
import { fetchRandomGif } from "./prebuilt-for-candidate";
const bus = new Bus();
bus.spy(console.log);

export const gifService = createService("gif", bus, () => fetchRandomGif);
Enter fullscreen mode Exit fullscreen mode

I said - Ok, now what is this library doing?

"Have you used Redux Saga, createAsyncThunk, or other async middleware? π—₯π˜…π‘“π‘₯ is a drop-in replacement, at less than half the bundle size. The bus receives events, which the service will put onto the bus as the effect starts, provides data, etc."

I knew Redux Saga. I said "The fetchRandomGif function - it's not written as a generator function or a saga, it just returns a Promise. Is that going to be compatible with your bus?"

"Yep, no problem. It can do Promises, Observables, iterables. It's just RxJS underneath.

If you've ever been frustrated using React Context or prop-drilling to share data across an app, a bus is a framework-free way to do the same. It's so easy I don't know why it's not built into every app!"

I did have prop-drilling and React Context issues, and I saw how events being visible to any part of the app by default would result in less code. That satisfied me of their choice, and I asked Chris to continue.

The State Model and Reducer

"Greatβ€” now let's start on our state model. It looks like there's only one field we need in state - the URL of the current image - is that right?"

I said "Are you forgetting the loading and error states?"

"One nice thing about an π—₯π˜…π‘“π‘₯ service is your state model doesn't need to include loading and error. You get into trouble when you mix transient fields like loading and error into state fields that you may want to persist for longer - like across sessions. Separate things that change at different rates, right?"

I had just dealt with a bug where a loading state loaded from local storage with a value of true - the spinner spun but nothing was happening. It occurred to me it was not really DRY to have a state field that isn't a direct reflection of whether a process is actually running, so I was ready to see it in action.

"We still need to see loading and error in the UI, but let's go ahead with a loading-free reducer."

The reducer looked like this:

const initialState = {
  url: "",
};

export const gifReducer = (state = initialState, e = {}) => {
  switch (e.type) {
    case "gif/next":
      return { ...state, url: e.payload };
    default:
      return state;
  }
};
Enter fullscreen mode Exit fullscreen mode

I noticed the strings in the case statements, and I said "These look like Redux Toolkit conventions, but with different names - where do they come from?"

"Fair question. A service has a standard set of actions, based on Observable life-cycle events. The next event delivers data, error an error, and started indicates a search began. There are typesafe versions of these too, do you want me to use them?"

I said, "Let's skip that for now and get our data in the UI."

"Cool. Then let's add the reducer to our service."

Chris changed the line to create the service ever-so-slightly, by adding the reducer.

- createService('gif', bus, () => fetchRandomGif);
+ createService('gif', bus, () => fetchRandomGif, () => gifReducer);
Enter fullscreen mode Exit fullscreen mode

"And now let's bring state into our UI".

UI Updates

Chris typed the following in a flurry of keystrokes..

import { gifService } from "./services/gifService";
import { useService } from "@rxfx/react";

function CatFetcher() {
  const { state, request } = useService(gifService);
  const { url } = state;

  <img src={url} alt="Animal GIF" />
  <button onClick={() => request()}/>
Enter fullscreen mode Exit fullscreen mode

I said "Let me get caught up. Through the useService hook, we have a reference to the state produced by the reducer. And we have a function with which to request the effect, which changes the state?

"Yeah, precisely!"

template with cat

It worked greatβ€” on the happy path. Now what about errors, I asked.

Errors

Chris hacked a thrown error into the fetch endpoint. Then captured a currentError field from the hook. Then used it in React to show the message.

-  const { state, request } = useService(gifService);
+  const { state, request, currentError } = useService(gifService);
Enter fullscreen mode Exit fullscreen mode
<div className="error">{currentError}</div>
Enter fullscreen mode Exit fullscreen mode

I tested that when an error was shown in the GIF fetcher, the error display just worked. And it was cleared automatically on the next click. I guess that's why the field is named "currentError"β€”once a new one begins there is no current error. A nice convenience.

template with error

After it had shown an error, it resumed future fetches just fine. I said to Chris "You pulled that off nicely. But since you left loading out of your state, how will we display that?"

Loading State

While the GIF is loading, let's change the text "Fetch Cat" to "Fetching.."

Chris captured the isActive field from the useService hook return value.

-  const { ... currentError } = useService(gifService);
+  const { ... currentError, isActive } = useService(gifService);
Enter fullscreen mode Exit fullscreen mode
<button onClick={() => gifService()}>
  {isLoading ? "Fetching." : "Fetch Cat"}
  {/* Fetch Cat */}
  <HourglassSpinner show={isLoading} />
</button>
Enter fullscreen mode Exit fullscreen mode

That isActive variable - the hook just knows that a fetch is active?

"Sure does. The service knows when it's doing work. It keeps a count of gif/started and gif/complete events and emits true when the count is > 0 and false otherwise."

I pretty much decided Chris had passed the interview, but to throw a challenge I asked about cancelation.

Cancelation and The Finished Product

I'd shipped many apps without cancelation before, especially before Abort Controllers. But I knew that to do top-notch UX, one had to be able to cancel effects to free up resources.

I asked how we could cancel a load while in progress. Chris added a Cancel button to the form, and I stepped out of the room for a second.

<button onClick={() => gifService.cancelCurrent()}>Cancel</button>
Enter fullscreen mode Exit fullscreen mode

When I returned, I opened up DevTools, and clicked Fetch Cat. I clicked Cancel, and BOOM, a canceled XHR on /search!

cat loading fixed

Chris showed the new fetchRandomGif function - which looked like the Promise-based version.

import { ajax } from "rxjs/ajax";

const fetchRandomGif = () => ajax.getJSON({
    url: "https://api.thecatapi.com/v1/images/search",
  }).pipe(
    map((r) => r.response[0].url)
);
Enter fullscreen mode Exit fullscreen mode

Seeing this, I asked, "So the service can just cancel this AJAX, even without an AbortController?"

"Cancelation is automatic- as long as the endpoint returns an Observable. It's crazy - every Observable since 2012 is cancelable, and yet today we have just Promises. It's nice that in π—₯π˜…π‘“π‘₯ you can return a Promise to start, and an Observable when you implement cancelation."

This was great. I made a mental note: Suggest the whole team learn about Observables and this API around them. Promises being run-to-complete by default started to look like a very bad idea, especially when it was easy as this to swap a non-cancelable AJAX with a cancelable one.

A Mysterious Departure

Chris had exceeded expectations on the first 3 mandatory points of the challenge. I wanted to hire, so I moved on to Chris's questions. We talked pleasantly, then when we were standing up to say goodbye, curiosity got the best of me, and I asked one more technical question:

"Just curious- but how would you handle a click while a GIF was already loading? Something like XState?"

Chris lifted their backpack to their shoulder and smiled.

"Oh, the π—₯π˜…π‘“π‘₯ service has that covered too. Just change the call to createService to createQueueingService and you're covered. I'll send you a CodeSandbox of it later today so you can try it out."

And with that, Chris was gone. And my learning into RxJS and π—₯π˜…π‘“π‘₯ had just begun.


Author's Note

Here's the CodeSandbox of the Cat Fetcher.

As you may have guessed, this was a fictitious story, written by me, Dean, the author of π—₯π˜…π‘“π‘₯ packages. I must stress that π—₯π˜…π‘“π‘₯ was not designed to handle interview problems, but real world ones! And it has been deployed to production in various forms for 4 years, solving problems like dynamic forms, 60FPS animation, Web Sockets and many more. I hope you will give it a look, and let me know what you think!

And soon there will be Part 2 where we address timeouts, maintaining the loading state until the bytes of the image have arrived, and other subtleties of data fetching.

-- Dean

Update: Here is part 2!

πŸ’– πŸ’ͺ πŸ™… 🚩
deanius
Dean Radcliffe

Posted on May 20, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related