Skip to content

Instantly share code, notes, and snippets.

@greim
Last active December 21, 2019 20:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save greim/3de3bcb71a672e11c75e371b7b81f4bb to your computer and use it in GitHub Desktop.
Save greim/3de3bcb71a672e11c75e371b7b81f4bb to your computer and use it in GitHub Desktop.
Vacancy Observers

Vacancy Observers

Vacancy observers are a data-fetching strategy for functional UI frameworks which doesn't rely on side effects. Instead, components are written as pure functions, indicating their remote data dependencies directly in their output using vacancies.

An observer may then detect the presence of those vacancies and take appropriate action, depending on the environment. For example:

  • In an Elm app: the observer might be a MutationObserver—running independently in JavaScript—which listens for vacancies in the DOM, makes HTTP requests, and feeds the results into Elm over a port.
  • In a React+Redux app: similar to above, the observer might be a MutationObserver which dispatches actions to Redux.
  • In a server-side renderer: The observer might be a post-processor which analyzes the HTML output to detect vacancies, fetch them, and use that data to either re-render the output, or http2-push to the client.
  • In a unit test runner: the observer would simply be the caller of the component, whose job would be to ensure the correct vacancies were being rendered.

Side Effects vs. Vacancy Observers

Here's a simplified illustration of how this compares to traditional side-effectful approaches.

image

  • Blue arrows represent unidirectional dataflow in your app.
  • Red arrows represent side-effects which trigger remote data reads.
  • Green text indicates where vacancies and observers live in the system.

How it Works

The overall system has two parts: vacancies and observers. Vacancies are part of your components’ rendered output, while an observer is something downstream of your component which “sees” vacancies and takes action.

For example, a React component might depend of a piece of remotely-fetched data to be passed in as a prop. Thus, it indicates that dependency in its rendered output as a data- attribute. React then renders that into the DOM.

Elsewhere, a MutationObserver sees that vacancy and fetches whatever’s necessary to fill it, dispatching the data to Redux. As a result, the component receives its remote data dependency in its props in a future render cycle. At no point did the component trigger a side effect.

Prior Art

For one example, HTML's <img> element is similar in concept to a vacancy observer.

<img src="logo.gif">

Its src attribute functions as a vacancy by indicating a remote data dependency. The observer is the browser itself, which detects the presence of the <img> and makes the necessary network requests in order to display the image. Lack of side effects just means that if you rendered the image in React (for example) you wouldn't need to trigger a fetch on the src, it just happens.

Why?

Since React first became popular, there's been an increasingly mainstream movement to use functional programming techniques in UI-building, and for good reason! Unfortunately, data fetching has been a roadblock in this area, due to the fact that something has to fork itself off of the dataflow line and perform that fetch.

Hence lifecycle methods, effect hooks, and various shims and abstractions—plus other shims and abstractions designed to make those shims and abstractions easier to work with—all to accommodate the need for side effects.

Vacancy observers arose out of a simple thought experiment: what would it take to write UIs that didn't actually need to use side effects for data-fetching, and thus didn't need all that extra runtime and testing complexity? You only have one choice, really, which is to include some sort of indication of your data needs in the return values of your component functions.

Implementation

The motel library provides a reference implementation. Vacancies are implemented as data-vacancy attributes, while the observer is a mutation observer. See that library's readme for more info on how it works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment