Introduction
Welcome to Refactor - Monad Style!. In this series, we’ll take an (admittedly trivial) web application and slowly refactor for composability. If you’re interested in following along you can clone the repository at https://github.com/BradleyDejong/refactor-monad-style. It really does help to make these practices sink in.
Before diving in, let’s briefly look at the characteristics of the code base. We’ve kept the dependencies rather lean (no nasty frameworks to spend 8 months learning); there’s just a utility for rendering DOM nodes (nanohtml
) and one for DOM diffing and updating (morphdom
or nanomorph
).
Libraries
I do want to point out that we could easily handle these two needs in plain JavaScript, but these libraries are very slick and let us focus on what matters.
Nanohtml
Who wants to hand-code element creation in plain javascript? Take a brief gander at MDN documentation for createElement
. It’s full of mutation, boilerplate and other nasty things. We can rewrite their example rather declaratively: var newDiv = html`<div>Hi there and greetings!</div>
. Much easier, takes care of sanitization, and even looks like HTML so we don’t have to mentally parse out what’s relevant DOM structure and what’s JS boilerplate (or at least not as much).
Nanomorph/morphdom
Essentially, we use this to dynamically update the DOM. Every rerender loop, we call morphdom(app, render({ state, dispatch }));
, which runs our render
function, then quickly figures out what needs to change between the last version. It mutates the DOM for us. Impure, I know, but useful. We’ll allow it.
Code overview
There are three main parts to our application. For now they’re all hosted in src/index.js
Views
These are simple functions that take the global state object and a function dispatch
with which components may dispatch redux-like events. You should be able to trace the code through fairly easily. While it’s not bad code necessarily, note all the manual passing of state
and dispatch
. It’s a lot of boilerplate. We’ll fix this up in the next article.
Example view:
const content = (state, dispatch) => html`
<div class="content">
${renderRefresh(state, dispatch)}
${renderDecorations()}
${renderClicky(state, dispatch)}
</div>
`;
Reducers
For now, this is just one function that handles all actions. If you’ve used any redux-like library (flux, redux, ngrx, etc), the pattern should be familiar.
const reduce = (state, event) => {
if (event === "clicked") {
return {
...state,
clicks: state.clicks + 1,
;
}else if (event === "update") {
} return {
...state,
lastUpdated: new Date(),
;
}
}return state;
; }
Impure stuff
This is just code to bootstrap the app and to update state/rerender when actions occur. We won’t spend too much time here.
const app = document.getElementById("app");
const state = {
lastUpdated: new Date(),
clicks: 0,
;
}
const rerender = (state, dispatch) =>
morphdom(app, render({ state, dispatch }));
const dispatch = (event) => {
const newState = reduce(state, event);
Object.assign(state, newState);
rerender(state, dispatch);
;
}
rerender(state, dispatch); // start the app
Wrap up
That’s the end of the code tour! In the next article, we’ll work on making views more composable.