Introducing FRETS, an Ultralight Frontend TypeScript Framework
If you’ve been doing javascript development on the web (or applied for any developer jobs recently), you know about React, Angular and Vue. Each of these is a modern, powerful, and well-maintained framework for creating interactive and rich javascript applications on the web. They attempt to make it easy to compose user interfaces of reusable parts for consistency. Usually you reach for a framework like this when you want to build a SPA (single page app). Sometimes the ecosystem, the tooling or community around the project is more compelling than the syntax and code architecture itself.
Libraries tend to grow many “necessary” appendages that bloat the size of your poject. Things like React “needing” babel and redux, Vue wants and vue-router and vuex and a template compiler. Then there’s the temptation to add these awesome pre-written UI packages like a material design implementation, or Element UI, and maybe a dozen other open source packages for API communication and date parsing and collection manipulation.
To be honest, I kinda love the chaos of this world. You get to learn constantly and you get to mix and match the best of everything, and there’s always some other neat trick of tooling that someone can teach. So, that’s why I don’t feel completely insane to say I’ve spent a little time lately developing another experimental basic framework for writing and organizing javascript app code.
Picard management tip: Even without game-changing results, experimentation is time well spent.
— Picard Tips (@PicardTips) October 23, 2017
My goal with this experiment was to learn more about the SAM pattern, that is State Action Model. As apposed to the more common MVC or MVVVM. It’s far closer to the React way of thinking, where each component of your UI is ultimately a render function. (You knew that’s what JSX compiles down to, right?) But, I am making the assumption from the beginning that it will only every be written in TypeScript, which opened up some interesting possibilities. What if we wrote a framework with the power of Vue or React but with all the power of TypeScript and a good IDE like VS Code as a given? Can we protect developers from the evil scourge that is memorized magic strings?
Even though TypeScript comes from Microsoft it is still an awesome well maintained open-source project that plays nicely with the rest of the JS community, and it’s super-power is giving JS developers the safety and consistency of a type-safe, compiled language without completely obliterating the aspects of JS that are interesting and good. Namely, functional programming.
So, my framework FRETS (Functional Reactive and Entirely TypeScript) is a set of classes and interfaces to make it easy for you to write code that complies (mostly) with the SAM pattern. You can get all the reassurance of reliable code completion and type checking while still writing “pure” functional code. I think classes made up of functions are a perfectly valid way of giving developers the convenience of automatic code completion, and the other advantages of the Typescript tooling world. Making a developer remember all the variable names or massive copy-pasting is the enemy of clean bug-free code.
To explain the framework, Let me work backwards through the SAM application pattern starting from the UI rendering in the browser.
Views
In SAM every piece of your UI should be a pure function that updates the DOM in some way. Reusability comes from classic refactoring and composition of functions, without learning any new ceremony of a component object structure. Your view rendering code should be modular and composable, these aspects tend to emerge as the developer starts programming and sees the need to refactor continuously.
I originally was playing around with Mithril and attempting to integrate it as a VirtualDom rendering implementation of the SAM pattern. But Mithril was not very TypeScript friendly, and a little searching revealed Maquette, a smaller and lighter TypeScript implementation of the hyperscript rendering interface that Mithril (and react) give us. It might even be more performant, depending on how you measure. It’s not perfect, but it is under active development and I think the value of a solidly implemented hyperscript rendering library, decoupled from the big projects, that we can build upon is of significant value.
Why no JSX? Why no templating? … This is an experiment, and in the past I have always been a believer in working with real HTML. I thougt staying close to the final implementation language was the smartest way proceed compared to things like ASP.Net WebForms or HAML. Every developer is familiar with HTML which is why Vue and JSX are so easy to learn, they have a declarative syntax that looks mostly just like HTML. But I wondered, can I avoid some of the pain of the syntactic restrictions of HTML? (let’s be honest, it is super verbose and repetetive). In the Mithril hyper script code I saw DOM rendering functions let you specify a CSS selector string — think of how you use the Emmet tool in your IDE to generate HTML.
div.main-component.theme-primary.green
These hyperscript methods take that selector string and an attributes object to generate an html element with all the appropriate class names, attributes, etc. I like it. It’s weird but it works. And as a developer I don’t have to do as much mode switching between HTML and JavaScript syntax.
What data does your view function render? Well, it’s just a plain old JavaScript object, or preferable a generic subset of that object if you’ve refactored your UI into smaller decoupled functions. Of course, since this is TypeScript our IDE will know that we’ve already specified the shape and types of the properties on that object, so we get code completion and type warnings everywhere we work with it reducing errors and making it easier to reason about your higher level code when you’re down in the rendering functions. Ideally you will have one big parent Class object for your entire application state making it easy to know what you’re passing around and looking for.
In FRETS you keep all your high level view functions that accept that global state data object in one class to make refactoring easy and painless.
State
State is a simple class that is responsible for calling those “View” render methods. You instantiate a new FRETS state object specifying the render function you want (with a default assumption that you’re using the Maquette projector for updating the dom). This state representer function will also recieve a preRender function to do any special calculations or logic for deriving transient properties in the application state from the values of the data properties object that it is passed. Things like warning messages, loading indicators, visibility switching, and in-app navigation or routing.
Model
The state was called by a function on your Model class called “present”. Generally there is one Present() function on a model, and it is tied to the one Render() function on you State class. This present function first executes any data validation logic that the model was configured with when we gave it a validate() function at instatiation. So the Model handles consistency, and this is also where you would specify data synchronization logic for communicating with a remote API.
Action
The Model was asked to update itself by a function on your Action class. This action class should be a new class that you wrote for this application which extends the FRETS ViewActions class. Your custom actions class will contain all the functions that your application might call to ever change data or state. These functions will have been bound to the event handlers on the dom, or timers or other reactive events. This practice makes sure you know exactly where to look for any change that was made to your application state, and you get code completion in your Views for this class because of the power of Generic Types.
Diving back down
Let’s follow the logic back down then.
Assuming we’re talking about yet another Todo list implementation: when your view function renders a button you will specify it’s onclick
handler as the function this.actions.createNewTask
which you already new about and stubbed in or wrote previously on that custom ViewActions Class.
When this function is called it will add a new task string to the array of tasks in a copy of the model properties. And then call the present()
function with those updated props. The present method runs your validation logic and either saves the errors in the data or saves it to a server, but either way it calls the render()
method on the State object with the new data properties object.
The render method checks for the existance of errors to display to the user, and it sets a couple derived properties on that object that the model doesn’t need to deal with, that will be used for changing what is finally rendered back out the user. At the end of it’s calculation work it will call your view rendering functions. But in the case of Maquette it’s actually just going to tell it’s own main Maquette Projector
to schedule a re-render on the next animation frame of the browser. Using Maquette in this way allows you to move certain really performance hindering calculations out to the view rendering functions if you want them to only ever be called once per render (every ~16ms) so we don’t bog down the browser. But I wouldn’t start doing this until you spot performance problems because this adds another place to look where state logic is happening.
The view method is doing the rendering of the list
1 | TodoList: (props: TodoListProps) => { |
Now, what about that h()
function… it requires a lot of “magic strings” and if there’s one thing I’ve been trained to hate it’s string literals in my code. So, what can we do about this? It’s generating HTML, but using classname syntax like Emment. And technically we could already know what those classnames are because they’ve already been written once… over in a css file.
So, we fire up a little code generation utility called frets-styles-generator that writes a templated TypeScript class file based on the contents of the css file in your project.
> node_modules/.bin/frets-styles-generator src/main.css src/base-styles.ts
And since we are using base.css (or another atomic css library like tachyons or tailwind) we want to have access to those in the TypeScript code which means they need to be exported members on a TypeScript object somewhere so the IDE can pick up on it. Then at the top of the Views file we import them
import { $, $$ } from "../base-styles";
Because it generates a convenient class containing a property for each css selector you might want, when you’re creating your markup in the views, where you would use the h()
function with a magic string, you can instead have a fluent api like:
h($$('button').btn.bgBlue.p2.mx1.$, {}, [])
But, I thought that was a little verbose and I can probably guess on the most common html tags I’ll be writing, so I added a few more helpers so you can do things like
h($.button.bgBlue.white.p2.mx1.$, {}, [])
But still: why do we have to use that ugly $
at the end to output the string selector that the h
function is expecting? Let’s embed our h function directly so we can extend it with our fluent API like so:
$.button.bgBlue.white.p2.mx1.h({}, [])
Awesome, now we’re talking! A terse fluent api that is generated directly from the selectors available in our CSS file already. That leaves just one more set of magic strings to deal with, the “classes” property e.g.
1 |
|
Well that’s repetetive and ugly, there’s logic in there and some magic strings that now don’t even match camelCased class names which have been used previously, breaking my mental model. So, inside the BaseStyles Class (proxied to $
) we also have some functions generating these CSS class name logic objects fluently. Note: you have to instantiate a new class each time because there’s internal state to deal with the conditional logic switching. You flip conditions inside of your fluent selector chain using $.when(condition).selectorNames.otherwise().differentSelectors
Otherwise flips the previous boolean or andWhen(condition)
will start a whole new condition chain.
1 | return $.button.circle.h({ |
Cool, now our only magic strings are in the text that is actually displayed on the page, and the correct way to refactor those out is to use a localization library like i18n. Which is a pain… so, we wont.
App Configuration and Setup
When it’s time to spin up your FRETS app you have to load in all the functions that you’ve prepared. I decided to call the App Constructor with a custom configuration object. It feels wrong to have an object play such a prominent role, but the object is full of functions… so I don’t think I strayed too far from home here.
1 |
|
The App constructor takes that configuration object as well as an optional initial state object for hydrating your app state.
1 | const app = new App<CalculationProps, ViewComponents>(container, { |
Once the app is ready we can call init()
with an array to specify elements in the page we are replacing with our highest level, parent view function.
1 | app.init([ |
You can replace and append dom elements anywhere on the page with different high-level functions and they will all get updated from the same shared state data.
So what does all of that cost us in terms of file size?
Maquette is 3.3kb gzipped and the FRETS base class code is only 800 bytes (really!) Gzipped.
That styles module is 4.75kb Gzipped, which is kinda annoying since the minified CSS file itself is just 5kb Gzipped. But I think it’s a useful tool for now and I could probably figure out ways to optimize it better in the future.
Add it all together and you’re looking at minimum of 8.85kb of javascript code being downloaded before you add in any extra vendor dependencies like velocity, moment, or lodash.
Item | Gzipped Size in KB |
---|---|
Maquette | 3.3 |
FRETS | .8 |
FRAMEWORK | 4.1 |
Generated Selectors Class | 4.75 |
TOTAL | 8.85 |
BaseCSS | 5 |
For an idea of how much code you will end up writing, it’s really hard to estimate. My custom application code for a medium-complexity application is coming in around 3.2kb gzipped.
Lessons Learned
Doing this experiment has tought me a lot. I feel a lot more confident in writing “functional” code now, and I am much more comfortable with the TypeScript system and the power of Generic types.
Now that I know it’s possible to have intelligent code completion for every important part of the framework it makes me hesitant to go back to Vue or React, with their less robust typescript support.
I understand the fundamentals of these VDom frameworks a lot better. I understand the SAM architecture a lot better too. Though I still suspect that there’s some critical aspect of that architecture that I’ve bastardized here. I had to make deicisions in favor of developer productivity while putting this all together.
I had the opportunity to think about JSX and it’s role in the React framework stack. I think it’s the most flexible and approachable tool for the job (as long as you’re shipping compiled code without a templating system) - but I think theres a value in building your HTML with a fluent API generated from the CSS classes provided by a good modular CSS framework. After all, JSX (and HTML for that matter) are essentially gigantic string literals full of stateful context that the developer has to hold in their head.
I learned about the new CSS standard variable syntax and used them through a PostCSS compilation workflow to compile my own version of BaseCSS. I also wrote a custom PostCSS based parsing tool to read that CSS, and I can say it was remarkably easy to use PostCSS, I don’t know why I’ve been so afraid of it and holding on to SCSS so tightly.
I had to learn a whole lot more about webpack config files, and the proper ways to make production bundle sizes shrink down. This knowledge will be immediately useful on almost any other project. Up until this point the webpack config was a little bit of a mystery to me. I don’t think I’m alone on that. But it is a really powerful tool, and now that I know what’s going on I can improve performance across many of my projects.
I plan to work on a boilerplate project to get you up off the ground if you wanted to try using it too. Let me know if you like this idea or if this whole thing seems totally crazy on twitter @sirtimbly.