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.
Here's a simplified illustration of how this compares to traditional side-effectful approaches.
- 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.
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.
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.
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.
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.