yarn add statorgfc
simple, plain JavaScript state management with built-in support for React.
Stator works by adding methods around a global plain JavaScript object, and attaching callback functions to it. The callback functions automatically call setState
on the Component(s) that need it. Read more.
It was developed as part of gdbgui and has recently been bundled into this library. gdbgui is 5749 lines of JavaScript in 41 files that create a frontend to a C/C++ debugger. It has many disparate components that need to update frequently and efficiently from websocket and AJAX data.
This should look very similar to React's API.
import {store} from 'statorgfc'
store.initialize({count: 0})
store.get('count') // 0
store.set({count: store.get('count') + 1}) // changed value to 1
store.get('count') // 1
Call store.connectComponentState()
in your constructor. That's it!
import React from 'react'
import {store} from 'statorgfc'
store.initialize({
numSheep: 10,
numWolves: 2,
numChickens: 90
})
class SheepWatcher extends React.Component {
constructor(){
super()
// connect global store to the state of this component
// this.setState will be called when 'numSheep' or 'numWolves' changes
store.connectComponentState(this, ['numSheep', 'numWolves'])
}
render(){
return {this.state.numSheep > this.state.numWolves ? 'all good' : 'watch out sheep!'}
}
}
// somewhere else in code far, far away...
store.set({numWolves: 15}) // SheepWatcher has setState() called
store.set({numSheep: store.get('numSheep') + 1}) // SheepWatcher has setState() called
store.set({numChickens: 100}) // No call to setState() in SheepWatcher
// because sheepWatcher isn't connected to the numChickens key
That's all you need to know to get started. There are a some other helper functions and options which you can read about below.
let watchers = store.getKeySubscribers()
console.log(watchers)
// {
// "numSheep": ["SheepWatcher"],
// "numWolves": ["SheepWatcher"]
// }
If a key in the store is not being acted upon by anything, that is useful to know too. You can probably (but not definitely) remove that key from the global store and simply store it statically somewhere else.
let no_watchers = store.getUnwatchedKeys()
console.log(no_watchers)
// ["numChickens"]
store.use((key, oldval, newval)=>console.log(key, 'is about to change'))
Read more on middleware below and in the API documentation.
import {store} from 'statorgfc'
store.initialize({count: 0})
store.subscribe(()=>console.log('store changed!')) // call anytime something changes, and log entire store
store.set({count: store.get('count') + 1}) // prints {count: 1}
store.set({count: store.get('count')}) // no callbacks triggered, because the value didn't actually change
See more examples.
Used for state management of a JavaScript application when any of the following are true
- the same piece of application state needs to be mapped to multiple container components
- there are global components that can be accessed from anywhere
- too many props are being passed through multiple hierarchies of components
If you can maintain the state of a component within the component itself you should not use statorgfc.
- Global State: State management occurs in one location -- a variable named store.
- Intuitive, Small API: Similar to React's, with no boilerplate necessary
- Typesafe changes: Values can only be replaced with data of the same type
- Immutable data... or not: You choose if you want to work with immutable data structures by setting a single boolean
- Efficient: If a value isn't changed during an update to the store, listeners aren't notified
- Optional Middleware: Use or write middleware to perform arbitrary validation, logging, etc. before store's updates occur
- Initialize once with
store.initialize({...})
- Subscribe components to keys with
connectComponentState(this, ['key1', 'key2', ...])
- Update the store with
store.set('key', val)
orstore.set({key: val})
. This should look familiar to React users. - Read the store with
store.get('key')
- Repeat step 3 and 4, and Components will automatically and efficiently re-render
There are some constraints built into Stator to ensure better usability.
- Keys cannot be added or removed after initialization
- Values must retain the same type during updates
- The store can only be updated with calls to
store.set()
. - The store can only be read with calls to
store.get()
. This returns a copy of the value, so when you update that copy, it does not affect the underlying store untilstore.set()
is called.
Methods on store
import {store} from 'statorgfc'
Initializes store. The initial value must be a JavaScript object with key/value pairs. Keys cannot be added or removed, and the values must retain the same type during updates. Returns nothing.
store.initialize({key1: 'val', key2: 'val'})
Options can also be set during initialization.
store.initialize({key1: 'val', key2: 'val'}, {immutable: false, debounce_ms: 10})
Change a value in the store. Similar to React's setState
, this is the only way in which the store should be changed. Also similar to setState
, an object can be passed in with the updated state. An error is thrown if the key does not already exist in the store.
store.set('mykey', newvalue)
store.set({mykey: newvalue}) // this syntax is okay too
Get value or reference to one of the keys in the store. If store.options.immutable
is true, the value is returned. Otherwise a reference is returned.
let mykey = store.get('mykey')
let myotherkey = store.get('myotherkey')
let entire_store = store.get() // with no arguments, whole store is returned
// ...update entire_store
store.set(entire_store)
Connect a react component's state to one or more keys in the store. Returns function that, when called, unsubscribes the Component from listening to those keys.
constructor(){
super()
this.unsubscribe = store.connectComponentState(this, ['showModal'])
}
render(){
return this.state.showModal // the Component's state automatically has the latest global value for all connected keys
}
componentWillUnmount(){
this.unsubscribe()
}
Connect a regular JavaScript function to a callback that is called only when one of a subset of the keys has been updated. Returns function that, when called, unsubscribes the function from listening to those keys.
let unsubscribe = store.subscribeToKeys(['myKey'], (keys_changed)=>console.log('myKey changed!'))
Return an object showing which keys are subscribed to by which functions and Components.
let watchers = store.getKeySubscribers()
console.log(watchers)
// {
// "key1": ["MyComponent1", "MyComponent2"],
// "key2": ["MyComponent3", "my_vanilla_function"]
// }
Returns an array of keys that do not trigger any callbacks when changed, and therefore may not need to be included in the global store.
let no_watchers = store.getUnwatchedKeys()
console.log(no_watchers)
// ["key1", "key2"]
Trigger a callback function after the store changes its value for any key. Prefer subscribeToKeys
over subscribe
since callbacks will be triggered less often and therefore make your application more efficient.
let unsubscribe = store.subscribe(function(keys_changed){
console.log('keys', keys_changed, 'changed!')
console.log('The store is now:', store.get())
}
)
Use a middleware function. If middleware functions returns true, the next middleware function will run until there are none left. At that point the store is updated and callbacks are dispatched. Otherwise, the middleware chain will stop, the store will not be updated, and callbacks will not be dispatched. Stator ships with some middleware, but any arbitrary function can be used.
Note all middleware functions are called with the same arguments and must conform to this function signature.
function logChanges(key, oldval, newval){
console.log(key, oldval, ' -> ', newval)
return true
}
function persistToLocalStorage(key, oldval, newval){
localStorage.setItem(key, JSON.stringify(newval))
return true
}
function abortIfFive(key, oldval, newval){
if(newval === 5){
// user wants to change value to 5. Don't let this happen.
console.log('aborting!')
return false // returning false aborts the store update entirely!
}
return true
}
store.use(logChanges)
store.use(abortIfFive)
store.use(persistToLocalStorage)
store.set('key', 3) // "key 0 -> 3", localStorage is set, store is updated.
store.set('key', 5) // "key 0 -> 5" "aborting!". store is not updated.
Options are a plain JavaScript object with the following defaults:
options: {
// when calling store.get() returns copy if true, otherwise reference
immutable: true,
// time to delay before notifying subscribers (callbacks) of a change
debounce_ms: 0,
},
They can be set manually or during initialization:
store.options.immutable = false
store.options.debounce_ms = 10
// or you can do this
store.initialize({key: 0}, {immutable: false, debounce_ms: 10})
They should be set before using the store and never updated after that.
Stator comes with built in middleware. See src/middleware.js for the full list of middleware functions.
You can use the built-in middleware like this:
import {store, middleware} from 'statorgfc'
// ...initialize store
store.use(middleware.logChanges)
store.use(middleware.persistToLocalStorage)
And of course you can write your own as needed.
MIT