A fine-grained reactivity solution sans compiling or bundling, allowing SPAs to be built without compilers, bundlers or even NPM. (~40 kB)
There are multiple pieces needed to build an SPA, but at the core of grainbox
is the reactivity:
- Wrap an object with
reactive()
to make it an observable. - Wrap a function with
reactive()
to make it recompute whenever an observable changes.
grainbox
uses the built-in Proxy object to box values. Functions that make calls to a proxy's getter are observers of that proxy, and calling a proxy's setter causes its observers to recompute.
grainbox
is a collection of pieces necessary to make a single page app (SPA) without compilers or bundlers, and possibly without NPM.
Sub-Package | Description |
---|---|
grainbox/reactivity |
Reactive state management similar to mobx . |
grainbox/history |
Reactive history. |
grainbox/routing |
Reactive routing. |
grainbox/hyperscript |
A custom implementation of hyperscript with support for es modules, and it adds support for these props: ref , onmount , unmount , disabled , checked , class . |
grainbox/html-tag |
If you are not using JSX (maybe because you are avoiding compilers or bundlers), you can use this html template tag literal which was made by combining grainbox/hyperscript with standard htm . |
Most of the functions are exported from a single place:
import grainbox from 'grainbox'
import {
reactive,
history,
registerRoute,
html,
h,
} from 'grainbox'
There are sub-packages which have additional exports.
They can be imported using a subpath or a direct path to the file in either one of the dist/esm
or dist/cjs
folders.
// subpath imports:
import * as reactivity from 'grainbox/reactivity'
import * as history from 'grainbox/history'
import * as routing from 'grainbox/routing'
import * as hyperscript from 'grainbox/hyperscript'
import * as htmlTag from 'grainbox/html-tag'
// direct imports:
import * as reactivity from 'grainbox/dist/esm/reactivity.mjs'
import * as reactivity from 'grainbox/dist/cjs/reactivity.js'
Using a CDN, NPM isn't needed anymore in order to build an SPA. It all just works, out of the box, thanks to ES Modules.
import grainbox from 'https://unpkg.com/grainbox'
Some points to make about delivery:
import grainbox
is ~30 kB. It is not currently minified.- Instead of using a CDN,
grainbox
can be used withweb-imports
to reliably servenode_modules
to the client.
grainbox
should be easy to pick up if you are familiar with observable-observer mechanisms. Here is a comparison against an example from mobx
:
import {observable, computed} from 'mobx'
class Proto {
@observable value = 0
@computed get valueAsString() {
return value.toString()
}
}
const obj = new Proto()
autorun(() => {
console.log(obj.valueAsString)
})
obj.value++
When obj.value++
runs, the autorun will log it to the console.
This how the mobx example above would be implemented using grainbox's reactive()
:
import {reactive} from 'grainbox'
const obj = reactive({
value: 0
})
const valueAsString = reactive(() => {
return obj.value.toString()
})
reactive(() => {
console.log(valueAsString())
})
obj.value++
// The only things that can be wrapped are objects and functions:
const ro = reactive({})
const rf = reactive(() => {})
const ro = reactive({value: 0})
const rf = reactive(() => {
// Calling the getter causes it to become linked.
return ro.value
})
reactive(() => {
// Calling a function will also cause it to become linked.
rf()
})
<body>
<script type="module">
import {reactive} from 'https://unpkg.com/grainbox'
const valueSpan = document.getElementById('value')
const store = reactive({value: 0})
reactive(() => {
valueSpan.innerHTML = store.value.toString()
})
window.add = () => {
store.value++
}
window.sub = () => {
store.value--
}
</script>
<span id="value">0</span>
<button onclick="sub()">-</button>
<button onclick="add()">+</button>
</body>
Run the code above: https://unpkg.com/grainbox/examples/fine-grained-reactivity.html
<body>
<script type="module">
import {reactive, html} from 'https://unpkg.com/grainbox'
let count = reactive({
counter: 0
})
// If the wrapped function's return value is a DOM element,
// reactive use its .replaceWith method to cause this DOM element to update.
const View = reactive(() => html`<span>Count: ${count.counter}</span>`)
const App = () => html`
<div>
<h2>counter using reactive</h2>
<${View}/>
<button
onclick=${() => {
console.log('increment')
count.counter++
}}
>
Click
</button>
</div>
`
document.body.appendChild(App())
</script>
</body>
Run the code above: https://unpkg.com/grainbox/examples/using-components.html
In addition to reactive
, there are additional functions which are exported from grainbox/reactivity
:
export {
reactive, // converts input into a proxy
isReactive, // checks if something was wrapped with reactive
fromPromise, // allows reactive functions to react to promises
hasDependent, // a isDependent on b
getDependents, // list of reactive objects and functions
getCreationContext // useful for checking identity
};
Usually, the reactivity solution is tied into history and routing. Included in grainbox
are solutions for these.
Supporting JSX currently requires compilation, however, browser may support it one day.
If you would like to use JSX instead of html
template tag literals, you can do so using the jsx-to-hyperscript
package. Then, h
must be present in any file which has JSX. This is similar to how React
has to be present in any file which has JSX.
// These imports are analogous to each other with respect to JSX being present in the file.
import {React} from 'react'
import {h} from 'grainbox'
// `jsx-to-hyperscript` will transforms this into: const element = h('div')
const element = <div/>
unpkg.com
uses theunpkg
field.esm.run
uses theexports
field, using thedefault
conditional.jsdelivr
uses themain
field.