-
-
Notifications
You must be signed in to change notification settings - Fork 151
Higher order components
@thi.ng/hdom supports different calling conventions for component functions. Which one to use largely depends on the nature of the component and is explained below.
Here is a simple link component:
// the first arg to a component function always is an
// injected arbitrary "context" object, which isn't used here...
const link = (_, href, body) => ["a", {href}, body];
Since this component is stateless, it can be used in the following equivalent ways:
// as so called "embedded" (or lazy) function
// the component fn & its args are wrapped in an array
// the function is only called later as part of
// the component tree processing
// note: the context arg is not specified here
// it will be injected automatically
["div", [link, "http://thi.ng", "thi.ng website"]]
// or direct/eager evaluation (at array contruction time)
// in this case we must provide a context arg ourselves (`null` here)
["div", link(null, "http://thi.ng", "thi.ng website")]
Many real world components require some form of internal setup procedure, either to prepare child components, event listeners, pre-compute attributes and / or initialize local / private state etc. Another hallmark of such components is that they often return a component function (a closure) themselves. (The need for returning a function arises from the dynamic state the component refers to.)
Here's one such example:
// higher order component w/ local state
const counter = (i = 0) => {
const attribs = { onclick: () => (i++) };
return () => ["button", attribs, `clicks: ${i}`];
};
Since we don't want to (or sometimes simply can't or shouldn't) execute this setup procedure over and over again in each new frame, these components force the same kind of pattern on all their parent components:
const app = () => {
// initialization
const c1 = counter();
const c2 = counter(100);
// return actual component as closure
return () => ["div", c1, c2];
}
// use the closure returned from `app()` as root component
start(document.body, app());
To avoid this as much as possible, it's best to keep as much state as possible outside components, e.g. in a central, single-source-of-truth state container as provided by @thi.ng/atom types.
That way we can pass values from the atom as args to the component using the "embedded function" calling convention shown earlier:
import * as atom from "@thi.ng/atom";
// counter values stored in central app state
const state = new atom.Atom({c1: 0, c2: 100});
const statelessCounter = (_, i) =>
["button", `clicks: ${i}`];
const app = ["div",
[statelessCounter, state.deref().c1],
[statelessCounter, state.deref().c2],
];
This is only half the solution. The remaining question now is how to update the counter values. Again, there're several options:
const state = ... // as above
// using cursors to specific values in the app state
const c1 = new atom.Cursor(state, "c1");
const c2 = new atom.Cursor(state, "c2");
// now takes a cursor as argument
const statelessCounter = (_, cursor) =>
["button",
{ onclick: (e) => cursor.update((x) => x + 1) },
`clicks: ${cursor.deref()}`];
const app = ["div",
[statelessCounter, c1],
[statelessCounter, c2],
];
Or for larger apps in a more scalable way, using @thi.ng/interceptors event handling:
import { EventBus, FX_STATE } from "@thi.ng/interceptors";
import { getIn, updateIn } from "@thi.ng/paths";
const state = ... // as before
// create event processor with given event handlers
const bus = new icep.EventBus(state, {
// event handler to update state value at given path
// handler args: state, event-tuple, bus, interceptor-context
"inc-counter": (state, [_, path]) => ({
[FX_STATE]: updateIn(state, path, (x) => x + 1)
})
});
// update counter component now taking context & path args
const statelessCounter = (ctx, path) =>
["button",
{ onclick: (e) => ctx.bus.dispatch(["inc-counter", path]) },
`clicks: ${getIn(ctx.state, path)}`];
const app = ["div",
[statelessCounter, "c1"],
[statelessCounter, "c2"],
];
// start hdom app with context object
// the root component function has been augmented
// to also execute any outstanding events
start(document.body, () => (bus.processQueue(), app), { bus, state });