Skip to content

Latest commit

 

History

History
230 lines (182 loc) · 7.58 KB

README.md

File metadata and controls

230 lines (182 loc) · 7.58 KB

AIMS

A tiny, stream-free* riff on @foxdonut's brilliant and elegant Meiosis pattern.

* That's right, no streams were harmed in the making of this package. But of course you can bring some of your own if you want.

What (and more importantly WHY) is it?

AIMS Is Managing State

I love Meiosis. I also love a nice godref. So here we are: AIMS uses the kernel of the Meiosis pattern, shallowly, to create both infrastructure and methodology for managing application state, without requiring users to be self-loathing or good at wrestling*. Oh and it's also just over 750 bytes with zero dependencies.

* Meiosis doesn't have these requirements either, but many other state management approaches do. You know who you are.

Installation

npm i aims-js

Properties

These are passed at instantiation to aims:

type description default
a function Accumulator: (x, y) => ({}) merge*
i object Initial state object {}
m function or array Mutators/Measurements: (state, patch?) => ({}) (or an array of these) []
s boolean Safemode false

* merge is a slightly modified port of mergerino by @fuzetsu.

Methods

These are attached to the returned aims instance:

usage description
get const foo = state.get() returns the current state
patch* ** state.patch({ bar: 'baz' }) uses the a function to apply the passed-in patch,
which in turn generates a whole new state

* In AIMS parlance, the word "patch" has dual meanings: as a verb, it's the method we use to "patch" our state with new values; as a noun, it's the object which provides those values. Try not to use both in the same sentence :) "Patrick, please patch our state with this patch."

** In safemode, patch is not a property of state, and instead is passed as the second argument to m.

Usage

Accumulator, Initialization, Mutators/Measurements, Safemode

Begin here:

import aims from 'aims-js' 
const state = aims()

Now state is ready to use. Give it some properties:

state.patch({ 
    name: 'Jack', 
    height: 'Short' 
})

Ok, now let's access our state:

const { name, height } = state.get()
console.log(name, height) // Jack Short

Accumulator function: a

Any function with the signature (previous_state, incoming_patch) => ({}) (e.g. Object.assign) will do:

// low-rent, shallow immutability
const state = aims({ 
  a: (prev, incoming) => Object.assign({}, prev, incoming) 
})

Initialization: i

Of course, in our first example, we could've set name, height at initialization:

const i = {
    name: 'Mike',
    height: 'Average'
}

const state = aims({ i })
const { name, height } = state.get()
console.log(name, height) // Mike Average

Mutators: m

Mutators are easier to illustrate than to explain:

const m = state => ({
    //  MUTATION FUNCTIONS: 
    //  apply patches to state
    
    setFirstName: firstName => {
        state.patch({ firstName })
    },
    setLastName: lastName => {
        state.patch({ lastName })
    },
  
    // MEASUREMENT FUNCTIONS: 
    // side effects, computations, and whatever 
    // else you want to be able to access via 
    // `state.myMeasurement(...)`
    fullName: () => {
      const { firstName, lastName } = state.get()
      return `${firstName} ${{lastName}}`
    },
    
})

const state = aims({ m: mutators })

/* ...somwhere in your code... */

onclick: e => { state.setFoo(e.target.textContent) }

Each mutator is a closure which accepts state as its parameter, and returns an object with state in scope. aims attaches the properties of each returned object to state, so calls can be made via state.myMethod(...).

You may have multiple, discrete sets of mutators, e.g. SocketMutators and RESTMutators.

const state = aims({ m: [SocketMutators, RESTMutators] })

In this case, it may be advisable to set namespaces, since aims is determinedly tiny and won't detect collisions for you.

// create the "Socket" namespace
const SocketMutators = state => ({
    Socket: {
        setFoo: foo => {
            state.patch({ foo })
        }
    }
})
// ...and the "REST" namespace 
const RESTMutators = state => ({
    REST: {...}
})

const state = aims({m: [SocketMutators, RESTMutators]})

// destructure state — NOT state.get() 
const { Socket } = state
Socket.setFoo('jack')

console.log(state.get()) // { foo: 'jack' }

Safemode: s

In larger codebases, it may be desirable to restrict mutations to actions only, eliminating occurences of state.patch({...}) within application views and elsewhere. Safemode achieves this by omitting state.patch and instead passing the patching function as a second parameter to Mutators, e.g.

const state = aims({
  m: (state, patch) => ({
    setFoo: foo => {
      patch({ foo })
    }
  })
  s: true,
})

Patch inspection

Sometimes there are imperatives associated with particular state changes. TodoMVC is a great example — every data change must be persisted, as must every filter change, which must change the URL for routing purposes. Rather than having several mutators each kicking off persistence and routing, we can use a custom accumulator to inspect incoming patches and respond accordingly, all in one place. A Mithril implementation might look like this:

import aims from 'aims-js'
const a = (prev, incoming) => {
    // update the route on filter changes  
    if (incoming.filter) m.route.set('/' + incoming.filter)
    
    // update localStorage with new state
    const new_state = Object.assign(prev, incoming)
    localStorage.setItem('todoapp-aims-m', JSON.stringify(new_state))
    
    return new_state
}

const state = aims({ a })

Integrating with view libraries*

* Unless you're using an auto-redrawing library or framework like Mithril.js, in which case you can skip this step.

To render your view, pass a function to the second argument of aims, which in turn takes state as its own argument, and render your App within the function. So for e.g. React:

import { createRoot } from 'ReactDOM'

const root = createRoot(document.getElementById('app'))

// Here the reference returned from `aims` is 
// unneeded, since we pass `state` to the function 
// provided, so we can just call `aims` directly
aims({}, state => {
  root.render(<App state={state} />)
})

Examples