resource

The engineering team is excited to announce the public alpha release of our new open-source library: resource.

We expect many improvements to come to the library before a completely stable 1.x release, but this library, as-is, has been powering all three of our main frontends for the past two years. We’re excited to see how it fares as open-source software: when it helps, when it frustrates, and what it becomes with a little community input.

Data fetching doesn’t need to be complicated

About three years ago, we started building out our core platform for internal users: the underwriters, servicing specialists, and other finance experts who make AlphaFlow a powerful capital partner. We chose to build our frontend in React against a REST API built in Go.

One of the first pieces of code we committed looked something like this:

const [portfolioData, setPortfolioData] = useState(null);

useEffect(() => {
    (async () => {
        const data = await (await window.fetch("/api/portfolio")).json();
        setPortfolioData(data);
    })();
}, []);

This commit was a big deal. Our front end was talking to our backend! Our app was showing live financial data! We were finally conquering Google sheets and realizing our technology vision!

But we also didn’t know how much we didn’t know.

First, we were missing a few edge cases, like a failed fetch or the component unmounting in the middle of the fetch. These were easy enough to accommodate.

const [portfolioDataFetchError, setPortfolioDataFetchError] = useState(null);
const [portfolioData, setPortfolioData] = useState(null);

useEffect(() => {
  let cancel = false;

  (async () => {
    try {
      const data = await (await window.fetch(“/api/portfolio”)).json();
      if (cancel) return;
      setPortfolioData(data);
    } catch (error) {
      if (cancel) return;
      setPortfolioDataFetchError(error);
    }
  })();

  return () => {
    cancel = true;
  };
}, []);

Second, and more challenging, we still had the rest of our portfolio page to build.

resource by AlphaFlow

First off, we can see the result of our basic fetch rendered as a list of loans. But we also have filters – so that fetch needs to take arguments of some sort and pass them back to the server.

And we have this bookmark feature – that looks like another fetch – and we’ll have to thread its results in with each loan list item.

And there’s this edit feature! We should probably re-run our search – and depending on where that “results” message is coming from, run another request to update that.

…that’s a lot! The portfolio fetching code we wrote was only step one. The whole idea that we can coordinate all of this from the body of a component might not pan out. Let’s imagine we push forward with that approach and try to handle a basic edit to a loan.

const [portfolioDataFetchError, setPortfolioDataFetchError] = useState(null);
const [portfolioData, setPortfolioData] = useState(null);

const handleEdit = useCallback(
  loanAfterEdit => {
    try {
      await window.fetch(“/api/portfolio/” + loanAfterEdit.id, {
        method: “PUT”,
        headers: {
          “Content-Type”: “application/json”,
        },
        body: JSON.stringify(loanAfterEdit),
      });
    } catch (error) {}

    try {
      const data = await(await window.fetch(“/api/portfolio”)).json();
      setPortfolioData(data);
    } catch (error) {
      setPortfolioDataFetchError(error);
    }
  },
  [handleFetchPortfolio]
);

useEffect(() => {
  let cancel = false;

  (async () => {
    try {
      const data = await (await window.fetch(“/api/portfolio”)).json();
      if (cancel) return;
      setPortfolioData(data);
    } catch (error) {
      if (cancel) return;
      setPortfolioDataFetchError(error);
    }
  })();

  return () => {
    cancel = true;
  };
}, []);

There are opportunities to DRY up this code. For example, the function that fetches our portfolio data could totally be one thing. But it’s going to need to be cancelable in the update as well as the write. When we do our clean-up pass here, this code is going to really grow in size. And when we add a new type of thing to fetch and update, the same. And if we need to share this data between components? That’s going to break this approach altogether.

This will technically work, but it feels more painful than it needs to be. The underlying behaviors we want aren’t nearly this difficult to express in plain language, can we get any closer to that?

Let’s imagine our ideal API

Reviewing the code above and our list of features, a few patterns start to emerge.

One is that we think of each kind of data differently. “Bookmarks” belong in a conceptual bucket separate from “Portfolio search results”. “Portfolio search results summary” kind of belongs in its own bucket, but it’s actually dependent on whatever “Portfolio search results” returns, even if there are two separate requests to get each kind of data.

And then there’s how we specify which member of each kind of data we’re trying to get. For the search results, it’s all of the search parameters. If we click into a loan to show a details page, it’s the ID of that loan.

This idea of “type of thing”, “key to get particular thing” has really good cognitive ergonomics. It’s nice that it handles both our list and show actions for loans in our portfolio. For bookmarks, there isn’t quite a “key to get a particular thing”, except perhaps the current user’s ID.

We ended up settling on two terms:

A resource is a type of thing. It’s the bucket containing a set of things that serve the same purpose or have the same shape.

An identity is the information we need to get a specific thing within a type. It can be search parameters or an ID or nothing.

And how about actual development ergonomics? What would we want the code that powers this to look like?

Well, in this developer’s experience, composing a component usually happens in this series of steps:

// 1. what am I going to call this thing?
const PortfolioPage = () => {};

// 2. what will it render?
const PortfolioPage = () => {
  return (
    <div className=“PortfolioPage”>
      <table>{/* … */}</table>
    </div>
  );
};

// 3. where am I going to get the data it needs to render?
const PortfolioPage = () => {
  const data = []; // <something amazing here>
  return (
    <div className=“PortfolioPage”>
      {data.map(loan => (
        <div className=“__loan” key={loan.id}>
          {loan.address}
        </div>
      ))}
    </div>
  );
};

Step 3, last time around, was where we wrote out not-quite-robust-enough fetching code, but this is definitely where it feels natural to write it. What if we condensed it down to something extremely simple, say:

// 3. where am I going to get the data it needs to render?
const PortfolioPage = () => {
  const data = PortfolioDataResource.use(searchParams);
  return (
    <div className=“PortfolioPage”>
      {data.map(loan => (
        <div className=“__loan” key={loan.id}>
          {loan.address}
        </div>
      ))}
    </div>
  );
};

To set up that resource, we’d only need to tell our library how to fetch it, something like:

const PortfolioDataResource = describeResource(async searchParams => {
  return await (
    await window.fetch(
      `/api/portfolio?${Object.entries(searchParams)
        .map(([field, value]) => `${field}=${value}`)
        .join(“&”)}`
    )
  ).json();
});

And what about my updates? How do I want to write those? Let’s break out a loan row component and see.

const LoanRow = ({ loan }) => (
  <div className=“LoanRow”>
    {loan.address}{” “}
    <SelectInput
      options={statusOptions}
      value={loan.status}
      onChange={
        {
          /* <something amazing here> */
        }
      }
    />
  </div>
);

Well, what would be really nice is if I could just pass a plain JS function to onChange and not have to think about coordinating its effects with other components.

const LoanRow = ({ loan }) => (
  <div className=“LoanRow”>
    {loan.address}{” “}
    <SelectInput
      options={statusOptions}
      value={loan.status}
      onChange={status => {
        setLoanStatusMutation({ loanId: loan.id, status });
      }}
    />
  </div>
);

And what might the inside of that function look like?

const setLoanStatusMutation = async ({ loanId, status }) => {
  try {
    // 1. actually save our changes
    await window.fetch(“/api/portfolio/” + loanId + “/setStatus”, {
      method: “PUT”,
      headers: {
        “Content-Type”: “application/json”,
      },
      body: JSON.stringify(status),
    });
    // 2. express which resources need an update
    await PortfolioDataResource.refresh();
  } catch (error) {
    window.alert(“We weren’t able to update the status on that loan!”);
  }
};

This is really appealing – one of the great things about React is how it makes one a better JavaScript developer. Its API surface area is small, it lets you use your existing JS knowledge to get things done. This is definitely a developer experience we’d like to copy.

One other thing we haven’t thought through is re-using data. Our bookmarks data is going to come back in the form of an array of loan ids, and we will be using that in each LoanRow to determine if something is bookmarked or not. What might that look like?

const LoanRow = ({ loan }) => {
  const bookmarks = BookmarksResource.use();
  const isBookmarked = bookmarks.includes(loan.id);

  return (
    <div className=“LoanRow”>
      <BookmarkIcon
        isBookmarked={isBookmarked}
        onSetIsBookmarked={isBookmarked => {
          setBookmarksMutation({
            loanId: loan.id,
            isBookmarked,
          });
        }}
      />{” “}
      {loan.address}{” “}
      <SelectInput
        options={statusOptions}
        value={loan.status}
        onChange={status => {
          setLoanStatusMutation({ loanId: loan.id, status });
        }}
      />
    </div>
  );
};

Of course, under the hood, we’re going to need to make sure we’re not re-fetching that bookmarks data for every component using it. They should all be using the same thing. In fact, there’s a lot of code we aren’t writing in our components that we’ll need to write somewhere. And, more importantly, there’s a lot of behavior we’re expecting implicitly that we should make explicit.

How should this library behave?

A few things we already know:

  • The library should be as decoupled from React as possible.
  • We want to avoid wrapping anything in a hook that can be a plain function.
  • Ideally, we don’t have to wrap the app in a top-level context or anything like that.
  • We want components that use the same resource of the same identity to have exactly the same data with only one fetch.
  • We want to guarantee that the data a resource hook is using matches its identity – the moment that thing changes we should be in a loading state.
  • We want mutation functions to be able to touch many different resources.

That’s a good starting point. It gives us some nice API constraints and starts to give shape to the internals of the library.

To start, it seems like the UI is going to be expressing to the library what data it needs in the form:

[
  {
    Resource: PortfolioDataResource,
    identity: searchParams,
  },
  {
    Resource: BookmarksResource,
    identity: undefined,
  },
  {
    Resource: BookmarksResource,
    identity: undefined,
  },
  {
    Resource: BookmarksResource,
    identity: undefined,
  },
  // … times the count of loans
];

It’s pretty straightforward to pull out the unique members of that array to deduplicate requests. Putting aside all the drama of async operations, our library will almost be a pure function of the UI’s data needs at a given time. The UI will declare its needs and the library will respond.

const libraryData = await Promise.all(
  filterDuplicates(dataNeeds).map(({ Resource, identity }) =>
    Resource.get(identity)
  )
);

The UI is an external surface, as long as we’re running the right requests and spitting out the right data, it (and by extension the developer) doesn’t have to worry about what’s happening within.

But we do need to make sure each piece of data is getting back to the right UI elements. That’s a bit challenging given how we want to separate our store from the UI. Maybe we could set up some kind of subscription in our Resource.use hook.

PortfolioDataResource.use = identity => {
  const [data, setData] = useState(null);

  useEffect(() => {
    dataStore.subscribe({
      Resource: PortfolioDataResource,
      identity,
      onChange: setData,
    });

    return () => {
      dataStore.unsubscribe({
        Resource: PortfolioDataResource,
        identity,
        onChange: setData,
      });
    };
  }, [identity]);
};

This will work: the subscription tells our library what data the UI needs, and the onChange callback lets it pass information back to the component that needs it.

Now, a layer deeper, we need to figure out what happens under the hood. What are all of the moving pieces the consumer of our library doesn’t think about and how do they fit together?

We already know that the UI is going to be sending us regular updates with the kind of data it needs, perhaps many times a second. We know those requests will be boiled down into the “surface” we need to accommodate. We also need to handle mutations – which may invalidate the data we already have in store and kick off refresh requests.

We can perhaps do clever things under the hood – like batching gets that come in at the same time, deferring gets until after a mutation finishes, when the data will be fresh. We can even implement caching relatively easily, where if something is no longer needed by the surface we can hold it around for a while. Or, if a component requests a resource with a different identity, we can scrap the last data we had immediately.

To handle all this, we might want some kind of central task controller that can handle our central actions and decide what to do when.

const taskTypes = {
  get: “get”,
  mutation: “mutation”,
};

const taskController = (() => {
  let taskQueue = [];
  let runningTasks = [];
  const runTask = async task => {
    runningTasks = […runningTasks, task];
    taskQueue = taskQueue.filter(queued => queued !== task);
    await task.run();
    runningTasks = runningTasks.filter(running => running !== task);

    if (!runningTasks.length && taskQueue.length > 0) {
      runTask(taskQueue[0]);
    }
  };

  const addToQueue = task => {
    taskQueue = […taskQueue, task];
    if (!runningTasks.length) runTask(task);
  };

  return {
    addTask: task => {
      if (task.type === taskTypes.mutation) {
        addToQueue(task);
      } else {
        runTask(task);
      }
    },
  };
})();

This version of the task controller only does one clever thing (running gets immediately and mutations in order), but we’ve got the brain of our library in place, and can teach it to do many clever things depending on our needs.

Finally, we thought we’d do a quick sanity check against the many well-liked JavaScript state management libraries that already exist. There were a lot of amazing options out there. The closest to what we were imagining were SWR and react-query. Apollo looked like the spiffiest and most full-featured. Recoil looked the most mind-bendy. Redux with thunks or sagas looked reliable.

The libraries that matched and shaped our mental models the most had a few things in common. Most importantly, they were not generalized solutions to state management, they were about interactions with server data, they had first-class examples of GETing and POSTing to a REST API. By extension, these libraries knew how to handle their async operations, and didn’t need any augmentation to work with async code. Also, they had some kind of very ergonomic solution to expressing what data a component requires in its body, like our Resource.use hook.

They kind of validated that our abstraction made sense, and helped us to clarify if our approach was different enough to even warrant a new library. But that did leave the question of why. Why do this work when you don’t necessarily have to?

The answer, very simply: we wanted to and we could make the time for it. This work wasn’t going to mean anything at all if we didn’t deliver our v1 release, so “we could make the time for it” was very important, and something we were always re-evaluating. If that changed, and we had a data fetching library but no app, I think we’d all feel pretty silly. The JS community has yet to see a company function with a “state-management-as-a-service” strategy. Conversely, though, having a project you and your team are genuinely excited about, that can be good, can create a whole lot of momentum on itself and related projects.

Introducing resource by AlphaFlow

After a few weeks of work for our v1 release – and then three more years of battle-testing and improvements. We’ve turned all of those initial ideas into a library that can handle almost anything we throw at it: resources.

Take a look at how you can fetch and write data, use data in multiple places without duplicated requests, handle pagination and infinite scroll at https://github.com/AlphaFlow/resource/tree/main/examples– everything you’d need to build the first version of our core admin product.

We’ve honed resources to work for our common use cases, and we’re ready to take the next step and battle test it against your use cases. This framework is robust enough to handle our data fetching needs, with the help of other users, we hope to make it sophisticated enough to handle any data fetching needs.

About the author:

Gus Nordhielm Gus Nordhielm is a part of the Frontеnd Engineering team at AlphaFlow. His goal is making complex processes simple to understand and control.

Prior to joining AlphaFlow, Gus was a Software Engineer at Lightstorm Entertainment – the production company behind the upcoming Avatar sequels.

back to top