An atom-based state management library that uses nubbin objects to track individual pieces of state.
When it comes to managing complexity, we're used to breaking larger things up into smaller things and composing those smaller things together to make software: projects get divided into folders and files, code is divided up into classes and functions, and pages are divided up into components. Likewise, we should divide state and actions up into small pieces and compose them together as much as possible. This makes it easier to share state, and only share the state you need, resulting in better code-splitting. Nubbins was also designed to interop with all of the popular frontend libraries so that you can keep all of your business logic state out of your view layer and easily switch between and/or swap out FE libraries with little friction. Updates are precise and performant.
// read-write nubbin
export const countNubbin = nubbin(0)
Read:
countNubbin.get()
// or~
countNubbin.value
Write:
countNubbin.set(5)
// or~
countNubbin.value = 5
Subscribe:
countNubbin.subscribe(value => {
// will be immediately invoked with the current value
})
countNubbin.observe(() => {
// won't be invoked until next update
})
Pass a pure function to the Nubbin constructor to create a nubbin that is readonly. It will track as dependencies any nubbins that are used in the function. After the first call to initialize, all read computations are lazy and memoized; if a nubbin has no subscribers, it will wait until its value is read before it will recompute it, and then it will only recompute if any of its dependencies change. This works even if the dependencies aren't all exposed right away in the getter function (i.e. conditionally read).
export const countNubbin = nubbin(0)
// Will track updates to countNubbin, but won't recompute until read
export const doubledNubbin = nubbin(() => countNubbin.get() * 2)
You can combine writable and readonly nubbins to create a nubbin that is writable internally but readonly externally. This is useful for syncing state with external sources, such as a server.
const internalNubbin = nubbin(0)
someExternalSource.subscribe(value => {
internalNubbin.set(value)
})
export const externalNubbin = nubbin(() => internalNubbin.get())
Creating and organizing numerous nubbins can get tedious. For convenience, you can create multiple nubbins with nubbinStore
and either leave them grouped together in a store or destructure them into individual nubbins.
export const dimensionsStore = nubbinStore({
width: 1,
length: 1,
area: () => dimensionsStore.width.get() * dimensionsStore.length.get(),
})
// or
export const { width, length, area } = nubbinStore({
width: 1,
length: 1,
// Yes, you can reference these!
area: () => width.get() * length.get(),
})
A useNubbin
hook is provided for each of these libraries. It operates much the same way as the useState
hook, returning a tuple with the current value of the nubbin as the first item and, if writable, the setter for the nubbin as the second item.
import { countNubbin } from './countNubbin'
import { useNubbin } from '@nubbins/react' // use @nubbins/haunted or @nubbins/preact for their respective versions
// ...someComponent
const [count, setCount] = useNubbin(countNubbin)
// read
const doubled = count * 2
// write
setCount(5)
Preact signal support is also provided with the useNubbinSignal
hook. This allows you to read and, if writable, set a reactive .value
property, and also leverage Preact's optimizations when passing a signal directly into its templates.
import { countNubbin } from './countNubbin'
import { useNubbinSignal } from '@nubbins/preact'
const SomeComponent = () => {
const count = useNubbinSignal(countNubbin)
// read
const doubled = count.value * 2
// write
count.value = 5
// pass signal in directly
return <input value={count} />
}
Solid support is provided via a nubbinSignal
utility which converts a provided nubbin into a Solid signal and returns a tuple with a getter and, if writable, a setter.
import { countNubbin } from './countNubbin'
import { nubbinSignal } from '@nubbins/solid'
const [count, setCount] = nubbinSignal(countNubbin)
// read
const doubled = count() * 2
// write
setCount(5)
A nubbinRef
utility is provided to transform a nubbin into a Vue ref, which has a reactive .value
property. This allows you to use two-way binding as you could with any other ref.
<script setup>
import { countNubbin } from './countNubbin'
import { nubbinRef } from '@nubbins/vue'
const countRef = nubbinRef(countNubbin)
// read
const doubled = countRef.value * 2
// write
countRef.value = 5
</script>
<template>
<!-- two-way bind -->
<input v-model="countRef" />
</template>
Conveniently, since nubbins follow the store contract of Svelte, you can use them directly in your Svelte components without any additional code by prefixing the nubbin variable with $
.
<script>
import { countNubbin } from './countNubbin'
// read
const doubled = $countNubbin * 2
// write
$countNubbin = 5
</script>
<!-- two-way bind -->
<input bind:value={$countNubbin}>
There are two utilities provided to utilize nubbins in Lit. One option is to use a NubbinController
which has get
and set
methods on it as well as a value
getter/setter property. Alternatively, if you have decorators enabled, you can decorate a LitElement property with nubbinProperty
to get a reactive property directly on the element.
import { html, LitElement } from 'lit'
import { customElement } from 'lit/decorators.js'
import { countNubbin } from './countNubbin'
import { nubbinProperty } from '@nubbins/lit'
@customElement('some-component')
export class SomeComponent extends LitElement {
@nubbinProperty(countNubbin)
// Not necessary, but good to infer type based on the nubbin's value
count = countNubbin.value
countController = new NubbinController(this, countNubbin)
render() {
// read
let doubled = this.count * 2
// or with controller
doubled = this.countController.get() * 2
return html`
<input type="number" .value=${this.count} @input=${this.handleChange} />
<p>Doubled: ${doubled}</p>
`
}
private handleChange = (e: Event) => {
const newValue = (e?.currentTarget as HTMLInputElement).valueAsNumber
// write
this.count = newValue
// or with controller
this.countController.set(newValue)
}
}
In cases where you're setting multiple nubbins at a time, it's highly recommended to wrap your updates in action
to reduce unnecessary updates to subscribers. This is especially useful for more complex computed nubbins. You can nest actions and the updates won't be made until the top-level action finishes. Any dependency updates needed within the action will still be available.
import { nubbin, action } from '@nubbins/core'
const width = nubbin(1)
const length = nubbin(20)
const area = nubbin(() => width.get() * length.get())
area.subscribe(console.log) // > 20
// Setting these individually will trigger area subscriber each time
width.set(2) // > 40
length.set(10) // > 20
// Whoops!
// But wrapping set calls in action batches the updates
action(() => {
width.set(4)
length.set(5)
})
// Since area is still 20 after the action completed, area's subscribers won't be updated
An options object can be passed as the second argument to the nubbin
function. The following options are available:
By default, nubbins will only notify subscribers if the new value is different from the previous value. This is done by comparing the new value to the previous value using the Object.is
algorithm. This means that arrays and objects will check for referential equality and won't update when using methods like Array.prototype.push
. If you want to customize this behavior, you can pass a custom hasChanged
function to the nubbin
function.
import { nubbin } from '@nubbins/core'
const arrayNubbin = nubbin([1, 2, 3], {
// When getting a new array reference, compare each value to see if any are different
hasChanged: (a, b) => a.some((value, i) => value !== b[i]),
})
arrayNubbin.set([1, 2, 3]) // won't notify subscribers
To provide an API that is familiar across various FE libraries, nubbin objects have both a pair of .get()
and .set()
methods and also a .value
getter-setter that aliases the .get()
and .set()
methods. The methods probably look familiar to users of Svelte stores and, to a lesser extent, Solid signals, while the .value
property probably looks more familiar to users of Vue refs and Preact signals.
While you are free to use either, or any combination of them for that matter, the methods are recommended because:
- It is explicit that your read or write action does other things (i.e. hooks into an FE library's lifecycle)
- Following off the last point, the set method allows passing a function to dynamically set the next value based on the current value that gets passed to the function. This technically is still supported with
.value
but would look more confusing e.g.countNubbin.value = value => value + 1
- The methods can maintain context in a destructuring assignment, allowing you to use them "disconnected" from the nubbin. Destructure assigning
.value
will just read the value immediately and not update the nubbin if you try to reassign the variable.