Grindelwald

Functional reactive programming with dark magic.
Grindelwald automatically tracks dependencies between your functions and only performs the minimal amount of computation when updates happen.
Grindelwald can replace RxJS and Bacon, except it's a tiny library that lets you write plain JS functions. You don't need to learn a whole new library to perform standard operations.
Grindelwald is perfect for memoizing functions that compute derived data from your Redux stores, similar to Reselect. It can even replace Redux (or MobX) entirely.
Examples
import {reactive} from 'grindelwald';
let start = 2;
const a = reactive(() => start);
const b = reactive(() => a() * 2);
const c = reactive(() => b() > 10);
const d = reactive(() => c() ? 40 : 20);
const e = reactive(() => c() ? 20 : 40);
const f = reactive(x => e() * x, x => x);
d()
d()
e()
e()
f(2)
f(2)
f(4)
start = 6;
a.update();
a()
e()
d()
f(2)
f(4)
start = 12;
a.update();
d()
e()
f(2)
f(4)
Subscriptions
You can subscribe to any reactive function with f.subscribe(listener)
. Your listener will be called any time f
returns a new value.
Adding a subscriber changes a reactive function (and all its dependencies) to no longer be lazy. This means that any time something changes, the function will immediately run and notify its subscribers, without anyone having to call it.
In other words, by default a reactive function is pull-based: it runs when called, like a regular function. Adding a subscriber downstream changes the function to be push-based: it runs whenever it's invalidated, and notifies its subscribers.
e.subscribe(value => console.log(`Subscriber got: ${value}`));
start = 10;
a.update();
start = 0;
a.update();
Autosubscribing
Sometimes we need to subscribe to a bunch of functions and run something when any of them changes. This is common in React, when a component depends on multiple pieces of data, and when any of them changes we need to rerender. Instead of explicitly calling subscribe()
on every function you depend on, Grindelwald can automatically subscribe for you to any reactive functions you call. Just wrap your code in an autosubscribe
call:
import {reactive, autosubscribe} from 'grindelwald';
let state = 1;
const a = reactive(() => start);
const b = reactive(() => a() * 2);
const c = reactive(() => b() > 10);
function onUpdate() { console.log('Something changed'); }
autosubscribe(onUpdate, () => a() * b());
autosubscribe(onUpdate, () => c() ? a() : b());
let state = 6;
a.update();
autosubscribe(onUpdate, () => c() ? a() : b());
autosubscribe(onUpdate, () => {});
Usage with React
It's easy to subscribe a React component to a bunch of reactive functions using autosubscribe
:
import {reactive, autosubscribe} from 'grindelwald';
let start = 1;
const a = reactive(() => start);
const b = reactive(() => a() * 2);
const c = reactive(() => b() > 10);
class Thing extends React.Component {
componentWillUnmount() {
autosubscribe(this.onUpdate, () => {});
}
onUpdate = () => {
this.forceUpdate();
}
render() {
return autosubscribe(
this.onUpdate,
() => <div>{c()}</div>,
);
}
}
You can extract this logic into a higher-order component, or – even better – a higher-order function that wraps a render function:
import {reactive, autosubscribe} from 'grindelwald';
function reactiveComponent(render) {
return class ReactiveComponent extends React.Component {
componentWillUnmount() {
autosubscribe(this.onUpdate, () => {});
}
onUpdate = () => {
this.forceUpdate();
}
render() {
return autosubscribe(this.onUpdate, () => render(this.props));
}
};
}
const Thing = reactiveComponent(props => <div>{c()}</div>);
Usage with Redux
A great use case for Grindelwald is to compute derived data from your Redux store. Derived data is then only recomputed when needed, preventing unnecessary rerenders of your React components.
import {reactive} from 'grindelwald';
import {createStore} from 'redux';
const store = createStore(reducers);
const getState = reactive(() => store.getState());
store.subscribe(() => getState.update());
const usersStore = reactive(() => getState().users);
const userById = reactive(id => usersStore()[id], id => id);
Once you have reactive functions that return data in the shape you want it, you can subscribe to them in your React components, as shown in the previous section.
Batched updates
If you have a function or component that subscribes to multiple reactive functions, it will be called only once per update()
call. This is convenient when connecting React components to data stores. When your store changes, a single update()
call can trigger the recalculation of many reactive functions and in turn multiple components. Grindelwald will batch all those updates together so your components only rerender once per change.
This also means it's an anti-pattern to update multiple reactive functions one after the other, as this might trigger multiple redundant updates:
import {reactive} from 'grindelwald';
let aState = 123;
let bState = 456;
const a = reactive(() => aState);
const b = reactive(() => bState);
aState = 234;
bState = 345;
a.update();
b.update();
Instead, you can make functions that frequently update together depend on a single function, like this:
import {reactive} from 'grindelwald';
let aState = 123;
let bState = 456;
const aAndB = reactive(() => [aState, bState]);
const a = reactive(() => aAndB()[0]);
const b = reactive(() => aAndB()[1]);
aState = 234;
bState = 345;
aAndB.update();
In practice, it's more common to have all your app's reactive function stream from a single state object, Redux-style. Just keep in mind that mutation is your enemy when using this pattern.
import {reactive} from 'grindelwald';
let state = {
a: 123,
b: 456,
}
const getState = reactive(() => state);
const a = reactive(() => state.a);
const b = reactive(() => state.b);
state = {...state, a: 234};
state = {...state, b: 345};
getState.update();
API
reactive(f: Function, argsToCacheKey?: Function): ReactiveFunction
Returns a reactive version of f
. It behaves just like f
, except its results are cached. f
runs again only if any other reactive functions it calls have run and returned a new value.
If f
takes any parameters, provide a second argument to reactive
which combines the arguments into a cache key string, e.g.: (arg1, arg2) => `${arg1}-${arg2}`
. f
will memoize its results based on the cache key provided. It will run again any time it's passed arguments it has never seen before, or after being invalidated (e.g. by a dependency changing).
ReactiveFunction.update(key?: string)
Invalidates the cache for this reactive function. This is useful to trigger updates to any functions that are not "pure", i.e. they depend on data that is not returned by other reactive functions. Typically you'll have at least one such function in your system. Calling update()
on it will invalidate all its dependencies.
To update a reactive function that takes arguments, provide the key to update. This should match the cache key generated by argsToCacheKey
above.
ReactiveFunction.subscribe(listener: Function, key?: string)
Subscribes listener
to a reactive function. listener
will be called with the return value of the function any time the value changes. You can optionally provide a key
to subscribe only to changes related to a specific set of arguments.
ReactiveFunction.unsubscribe(listener: Function, key?: string)
Unsubscribes a function previously subscribed. Make sure you pass in the same function instance as you did to subscribe
.
autosubscribe(onUpdate: Function, f: Function)
Immediately runs f
, and returns its return value. Also keeps track of any reactive functions f
calls, and adds onUpdate
as a subscriber to all of them. On subsequent calls, if the dependencies of f
change, the subscriptions get updated. Therefore, calling autosubscribe(() => {}, onUpdate)
unsubscribes onUpdate
from everything.