Skip to content

Instantly share code, notes, and snippets.

@drodsou
Last active February 20, 2022 05:30
Show Gist options
  • Save drodsou/3856fca1e9da003d7c9c825ee3497be7 to your computer and use it in GitHub Desktop.
Save drodsou/3856fca1e9da003d7c9c825ee3497be7 to your computer and use it in GitHub Desktop.
Minimal vanilla javascript state/actions store (Elm inspired, compatible with Svelte store and React/Preact)
/**
* Minimal vanilla javascript state/actions store (Elm inspired, compatible with Svelte store and React/Preact)
* use the same reactive store no matter the framework you happen to be using
*/
export function createStore(state={}) {
// -- subscriptions
const subs = new Set();
function runSubs (actionName, actionArgs) {
for (let sub of subs) {
sub({state: store.state, computed: store.computed, actionName, actionArgs});
}
}
const store = {
state: state,
computed: {},
//-- we watch actions, not state changes per se, so you're free
//-- to make several state changes per action without triggering subscriptions/renders
//-- also actions are more concise and readable, as state change is straightforward
action: new Proxy( {}, {
get (actionObj, actionName) {
return function (...actionArgs) {
actionObj[actionName].apply(store, actionArgs);
runSubs(actionName, actionArgs);
}
}
}),
// svelte compatible for state AND computed, without need for derived stores (!)
subscribe(fn, runOnSubscribe=true) {
subs.add(fn);
if (runOnSubscribe) { fn({state:store.state, computed: store.computed }); } // svelte expects fn to run on subscription by default, to get store value (!)
return ()=>subs.delete(fn); // unsubscribe
},
}
return store;
}

See:

  • createStore.js for the base library
  • createStoreExample.js for example store creation

And now how to subscribe/update UI in JS / React / Svelte:

vanilla js subscribing

<button onClick="store.action.inc()"></button>

<script type="module">
  import store from 'createStoreExample.js';
  window.store = store;
  store.subscribe(({state,computed})=>{
    document.querySelector('button').innerText = state.count;
  });
</script>

Or more declarative/sofisticated, you can use optional createStoreVanilla.js helpers for autobinding

React/preact subscribing

import store from './createStoreExample'; 

constructor () {
  ...
  store.subscribe(({state,computed})=>{
    this.forceUpdate();  
  });
}

Or if using hooks use createStoreReact.js

import _store from './createStoreExample';
import {useStore} from './createStoreReact';

function Component () {
  const store = useStore(_store);  // or {..._store} for independent one
  return <button onClick={()=>store.action.inc()}>{store.state.count}</button>
}

Svelte subscribing

As store.subscribe function is compatible with native Svelte stores interface, nothing special is needed to use Svelte $ autosubscriptions, both for state and computed props:

Several .svelte files could share same common store, of course

<script> import store from './createStoreExample.js'; </script>

<button on:click={()=>store.action.inc()}>{$store.state.count}</button>
<div>{@html $store.computed.countHtml()}</div>
// -- use example
import {createStore} from './createStore.js';
export default const store = createStore({count:1});
// -- state should be only changed inside an action, but it is not enforced
// -- (state is a plain, not proxied, object, so direct change outside of actions wont trigger subscritions)
store.action.inc = function () {
this.state.count++; // straightforward state manipulation, synchronous (react!) and
// no cumbersome state update function, receiving state + new state object merging + returning mew state
// actions are bound to its store, so this works ok
};
store.computed.countHtml = ()=>`<b>${store.state.count}</b>`;
app.computed.color = (color1='red',color2='green')=> app.state.count % 2 ? color1 : color2;
// -- to use a store from createStore.js in React hooks
export function useStore (givenStore) {
const selfForceUpdate = useReducer(x=>!x, false)[1]; // can't be inside a conditional;
useEffect(() => {
const unsubscribe = givenStore.subscribe(selfForceUpdate, false);
return () => { unsubscribe(); }
}, []);
return givenStore;
}
// -- optional additional automatic subscriptions and action listeners for vanilla javascript
// -- use: store.subscribe(dataBindSubscriptions);
// -- eg: <button data-bind="innerText:state.count; style.color:computed.color:yellow:green"></button>
// -- eg: <ul data-bind="innerHTML:computed.list:3"></ul>
// -- in 'data' is passed both state and computed from store
export function dataBindSubscriptions ( data=>{
for (let e of document.querySelectorAll(`[data-bind]`)) {
for (let part of e.dataset.bind.split(';')) {
const [target, source, ...args] = part.trim().split(':');
if (!target) { continue; }
let render = new Function('e','data','args',
source.includes('state.') ? `e.${target} = data.${source}` : `e.${target} = data.${source}(...args)`
);
try { render(e, data, args) }
catch (e) { console.error(`ERROR: Not found data-bind: ${target}:${source}`); console.log(e)}
}
}
});
// -- auto add event listeners (once), if global window.app / window.store is not wanted
// -- use: addActionListenersDataOn(store);
// -- eg: <button data-on="click:inc"></button>
export function addActionListenersDataOn (store) {
for (let e of document.querySelectorAll(`[data-on]`)) {
for (let part of e.dataset.on.split(';')) {
const [event, action,...args] = part.trim().split(':');
if (!event) { continue; }
try { e.addEventListener(event, ()=>store.action[action](...args)) }
catch (e) { console.error(`ERROR: data-on: ${event}:${action}:${args}`); }
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment