__ __ _____
/ / / / / ___ \ ( Logo inspiration from MostJS
/ / / / / / / / https://github.com/cujojs/most )
/ /__/ / / / / /
\_____/ /_/ /_/
Unframework for Universal Uncomponents
We do not think in terms of reusable components. Instead, we focus on reusable functions. It is a functional language after all! -- Scaling The Elm Architecture
Install the un.js
package with
$ yarn add un.js
or with
npm install un.js -S
or with pnpm
pnpm install un.js -S
and try the active counter example or read the introduction below.
- Write your business logic as pure functions with no external dependencies
- No side-effects, testable by your favorite test runners
- No external imports, no packages, no libraries
- No extension of proprietary component classes (Backbone, React, ...)
- No lock-ins, your uncomponents should be usable as plugins with or without any framework
- Reuse your code and share with others accross frameworks with no boundaries
-
Frameworks try to provide and cater for everything,
un
tries the opposite - give you the maximal possible freedom. -
Frameworks try to tell you exactly what to do,
un
tries the opposite - staying out of your way. -
Frameworks make your code coupled with proprietary syntax,
un
lets you write your code with plain JavaScript functions, undistinguishable from any other functions. There is not a single trace ofun
in any of your functions. -
Frameworks often like you to inherit from their proprietary classes,
un
tries to help you embrace the pure functional style and minimise use of classes andthis
. However, this is merely suggestive. Giving you maximum freedom and staying out of your way is a higher priority forun
.
Currently a single tiny factory function called createMount
. See here the complete code. Its role is similar to React.render
, in which you would typically see it in only few places in your app.
Here is a usage example. Instead of learning new API, new framework or long set of new methods, your simply import your favorite familiar libraries that you are already using anyway:
const mount = createMount({
// your favorite stream factory
// mithril/stream, TODO: flyd, most, xstream
createStream: require("mithril/stream"),
// your favorite element creator
// mitrhil, TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript
createElement: require('mithril'),
// your favorite create tags helpers (optional)
createTags: require('hyperscript-helpers'),
// mithril.render, TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith
createRender: element => vnode => require('mithril').render(element, vnode)
})
So instead of having external dependencies in every file,
un
simply lets you provide those libraries once and return the mount
function, the only function from un
that you need. The role of the mount
is similar (and inspired by) Mithril
m.mount
or React.render
with auto-redrawing facility. Our key vision is, attaching a live component to an element should be as simple as calling a function and mount
does exactly that:
// mount our live uncomponent and get back its writeable stream of actions
const actions = mount({ element, reducer, view, initState})
So we call mount
with 4 basic properties:
-
element
: HTML element, where we attaching our uncomponent, similar toReact.render
the element's content will be overwritten -
reducer
: Redux style reducer from our model logic -
view
: Plain pure function takingdispatcher
andstate
and returning newstate
, the state can be global, narrowed down, or completely local to the uncomponent, to cater for the fractal architecture. The view function dispatches actions just like in Redux and returns a virtual or real DOM element, depending on the library used in configuring themount
. But to be completely pure with no external dependency, theview
must include the element creator factory as one of its parameters:
// all parameters are explicit, no dependencies, no magic
// can be tested as pure dumb function in any environment
const view = h => (state, dispatch) =>
h('div', `Hello World, your ${state} is wonderful!`)
where h
stands for our favorite element creator passed to createMount
. We find the hyperscript
API supported by many libraries (e.g. Mithril, Snabbdom, or react-hyperscript
) most convenient, but using JSX should also be possible as it is equivalent to the React.createElement
calls.
Or use the createTags
helpers (like hyperscript-helpers
) that you can conveniently destructure inside the view:
// again, no dependencies, only function parameters,
// all inputs are instantly visible, no need to jump elsewhere
const view = ({ div }) => (state, dispatch) =>
div(`Hello World, your ${state} is wonderful!`)
The other two parameters of the view
are dispatch
and state
that match the types (or more precisely, interfaces) of the action
and the state
parameters of the reducer
. (Note how the view
signature matches the one of the reducer
. Further, it also matches the native JS Array.prototype.reduce
as well as the general reduce
method signatures, the latter provided by the Foldable Typeclass.)
The state, updated by the reducer, will be passed directly to the view (from the same mount
call). And every action value used to call the dispatch
function, will be passed directly as action to the reducer
. For example, calling dispatch('foo')
in the event handler inside the view
will result in foo
being passed as action
to the reducer
.
This style of writing was inspired by https://github.com/ericelliott/react-pure-component-starter and https://medium.com/javascript-scene/baby-s-first-reaction-2103348eccdd
In React
the role of the view
would be played by the component render
method, but we already have another static method React.render
, so we prefer to call it the view
as in Mithril.
initState
: The state to initialise our uncomponent.
Why "uncomponent"? Because there isn't really much of a "component", the reducer
and the view
are just two plain functions and the initial state is a plain value.
- Native JavaScript functions. Or native generators. Or native object holding a few functions.
- No proprietary syntax. Every part of the framework, library, package is hidden away from the user. In the configuration.
- Pluggable into any framework. Or into no framework. This is what configuration is for.
Streams are in the core of un
. The introduction to Reactive Programming you've been missing by Andre Staltz is a great introduction to streams. flyd
is a great minimal but powerful stream library to use, including great examples to see the streams in action. The Mithril stream library is even smaller but suffices to let un
do its job. Note that some libraries, such as most
distinguish between "pending" and "active" streams, but to make things as simple as possible, all streams in un
are always active, readable and writeable.
Despite of their initial complexity, streams model very well the asynchronous processes such as user acton flow, and consequently help to greatly simplify the architecture. The state values are stored directly inside the stream, so no stores such as in Redux are needed.
Instead of letting the framework do some "magic" behind the scene, when updating the DOM, with un
, your view listens to its state stream. Whenever the state changes, its new value is passed to any subscriber, or which the view is one. The view function is pure with no side-effects, so all it does is pass the new updated element to the rendering library you provided to
to createMount
. It is then the library's responsibility to create the side-effect updating the DOM.
Note that you can absolutely ignore the stream part and write your code without seeing a single stream. Like in Redux, every action passed to dispatcher will go through the cycle. However, using streams can give you additional control. The mount
method returns the action stream that is both readable and writeable. That means, you can attach other subscribers to your actions, or you can actively pipe new values into it, causing the same or additional actions passed to the reducer and subsequently updating the DOM.
A basic example below is demonstrating how the action stream can be externally driven in addition to user actions.
The un
mount function, created as described above,
returns for every uncomponent, the object
const {
states: streamOfStates,
actions: streamOfActions
} = mount(...)
holding the state and action streams (we like to refer to streams by plurals to emphasize their collection nature).
That means, you can conveniently add any complex behavior (such as loading external data) to your uncomponent by piping the external actions into its action stream, or you can attach an external subscriber to the state stream, to be updated on any state changes in a reactive fashion.
The active-counter example demonstrates this feature, see below.
Right now the streams provided by un
conform to the Mithril Stream API
In order to make using un
as universal and painless as possible,
and accessible to broader audience, we would like to facilitate plugging other stream libraries.
So you can use your favorite stream api to control your uncomponents.
Our both state and action values are just numbers and the reducer simply adds the action value to the state:
const reducer = (state, action) =>
state + action
By making the state local and avoiding giving specific names to the actions,
we can make the reducer function more of a general purpose and reusable.
Just like a function in Ramda
or similar library.
Our view function example here demonstrates how a function helper inside can reuse all the arguments of the outside function:
const view = ({ button }) => (state, dispatch) => {
// reusing the button function
const change = amount =>
button(
{onclick: () => dispatch(amount)},
(amount > 0)
? `+${amount}`
: `-${-amount}`
)
return [
`Increasing by 5 every second: `,
change(10),
` ${state} `,
change(-10)
]
}
Here we attach to the onclick
listener (following Mithril
s API flavour)
the anonymous function passing the amount
to the dispatch
.
As mentioned above, that value is passed as action to the reducer.
Behind the scene, every time the user clicks that button,
the amount
value is written into the action
stream.
That is essentially what the dispatch
function does.
In addition to user's actions, the counter is being updated from the application via this code:
const delayedConstant = (val, delay) => stream => {
setInterval(() => stream(val), delay)
return stream
}
delayedConstant(5, 1000)(actions)
The actions
stream here is exported from the un
mount
function
and gives access to the external drivers.
The simple periodic values here are for demonstration purposes.
They can be replaced by any HTTP request or any value returned by a JS Promise,
or can be even subscribed to another stream, such as event stream from any event:
// the response value from the promise will appear in the actions stream
// once the promise is resolved or the error object if the promise is rejected
actions(fetch(someUrl))
// every value from the `externalStream`
// will be passed down the actions stream in real time
externalStream.map(actions)
The functionality here is based on the
getter-setter syntax of the flyd
stream library and the way promises are treated.
However, any another stream library with stream updates functionality
(e.g. mostjs-subject
)
can be used instead.
Once subscribed as above, all values will be automatically passed to the reducer as action values. That way all business logic needed can be put as pure function updating the state inside the reducer.
The rest is full automatic: reducer runs on every new action, the state is updated and passed to the view that will be sent to the renderer to update the dom.
// the only method you ever use from 'un'
const createMount = require('un.js')
// or React.createElement
const mount = createMount({
// your favorite stream factory
// TODO: flyd, most, xstream
createStream: require("mithril/stream"),
// your favorite element creator
// TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript
createElement: require('mithril'),
// your favorite create tags helpers
createTags: require('hyperscript-helpers'),
// TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith
createRender: element => vnode => require('mithril').render(element, vnode)
})
// create dom element
const e = document.createElement('div')
document.body.appendChild(e)
// mount our live uncomponent and get back its writeable stream of actions
const actions = mount({ e, reducer, view, initState: 0 })
The Basic Examples are intentionally made very simple and focused.
The submit example demonstrates how to attach an simple update action to the onsubmit
event of the <form>
.
The submit-with-reset example in addition resets the input field after submission by providing additional state variable to control it.
The keypress example demonstrates how to pass all keys pressed to the dispatcher, and then via the reducer, back to the state.
The todos example demonstrates a basic interactive todos collector.
The todos-with-delete example adds the delete feature to the previous example. It deviates slightly from the traditional todos examples, in which the deletion is done by name, i.e. all todos with the same name are deleted at the same time.
Professor Frisby's Mostly Adequate Guide to Functional Programming by the same author.
Professor Frisby Introduces Composable Functional JavaScript
The introduction to Reactive Programming you've been missing by Andre Staltz, as well as many other of his always enlightening posts as well as his CycleJS project
@sindresorhus on reusable modules
The Ramda and its sister Fantasy Land and Sanctuary projects
The simple but powerful flyd
stream library
The new Functional Reactive Turbine
framework and the oder Functional Frontend Architecture
by @paldepind
The Snabbdom virtual dom library and the broader Hyperscript Ecosystem
TodoMVC example built with most.js
...To be extended...