Skip to content

Instantly share code, notes, and snippets.

@barelyhuman
Last active June 19, 2025 07:10
Show Gist options
  • Save barelyhuman/3d3f19f0530383b736213eac96fe7a7b to your computer and use it in GitHub Desktop.
Save barelyhuman/3d3f19f0530383b736213eac96fe7a7b to your computer and use it in GitHub Desktop.
Toy FP Example

Simple toy implementation of what a functional programming based UI library would work like, we have matured variants of this in the ecosystem (Cycle.js, Moonjs), this just isolates the impl of fn(state)-> view to make it easier to understand for others since UI dev is considered to be super complicated by most devs I've talked to.

Diffing

The majority of the work, for rendering the view is handled by snabbdom for sake of simplicity. You can write your own vdom diffing and patching but this just makes it easier to explain the concept instead of throwing a lot of knowledge at once.

import {
classModule,
eventListenersModule,
h,
init as initDOM,
propsModule,
styleModule,
} from "https://esm.sh/snabbdom";
export { h };
const patch = initDOM([
classModule,
propsModule,
styleModule,
eventListenersModule,
]);
export function init(
model,
update,
view,
mountTo,
) {
const initialModel = model();
const container = mountTo();
let prevView;
const createDispatcher = (baseState) => (msg, payload) => {
const state = update(msg, payload, { ...baseState });
const nextView = view(state, createDispatcher(state));
patch(prevView, nextView);
prevView = nextView;
};
prevView = view(initialModel, createDispatcher(initialModel));
patch(container, prevView);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@unocss/reset/tailwind.min.css" />
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/@unocss/runtime"></script>
<script src="./index.js" type="module"></script>
</body>
</html>
import { h, init } from "./h.js";
function model() {
return {
count: 0,
};
}
function view(state, update) {
return h("div.p-3.flex.flex-col.items-center.justify-center", {}, [
h("p", {}, state.count),
h("button.min-w-[150px].bg-black.text-white.px-3.py-2", {
style: {
"cursor": "pointer",
},
on: {
click: () => {
update("DEC", undefined);
},
},
}, [
"-",
]),
h("button.min-w-[150px].bg-black.text-white.px-3.py-2", {
style: {
"cursor": "pointer",
},
on: {
click: () => {
update("INC", undefined);
},
},
}, [
"+",
]),
]);
}
function update(msg, payload, state) {
switch (msg) {
case "INC": {
return {
...state,
count: state.count + 1,
};
}
case "DEC": {
return {
...state,
count: state.count - 1,
};
}
}
return {};
}
init(
model,
update,
view,
() => document.querySelector("#app"),
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment