import { useState, useEffect } from "react"; const createObservable = initial => { let _val = initial; let _listeners = []; return { _get: () => _val, _set: newValue => { const oldValue = _val; _val = newValue; _listeners .filter(({ equality }) => !equality(oldValue, newValue)) .forEach(({ callback }) => callback(newValue)); }, _sub: (callback, equality) => { _listeners.push({ callback, equality }); return () => { _listeners = _listeners.filter(ln => ln.callback !== callback); } } } } const nameToHookName = name => "use" + name.chatAt(0).toUpperCase() + name.slice(1); const shallowEq = (x, y) => x === y; const createHook = obs => { // allow linters to pick this up as a hook const useObservable = (equality = shallowEq) => { const [val, setVal] = useState(obs._get()); useEffect(() => obs._sub(setVal, equality), [equality]); return val; } return useObservable; } export const createStore = (initial, actions = {}) => { const store = {}; // maps keys to observables const hooks = {}; const mergeState = newState => { Object.entries(newState).forEach(([key, newValue]) => { const obs = store[key]; if (obs) { obs._set(newValue); } else { const newObs = createObservable(newValue); store[key] = newObs; hooks[nameToHookName(key)] = createHook(newObs); } }); }; mergeState(initial); const dispatch = Object.fromEntries(Object.entries(actions).map(([name, action]) => [name, (...args) => mergeState(action(...args))])); const selector = Object.fromEntries(Object.entries(store).map(([key, obs]) => [key, obs._get])); return [hooks, dispatch, selector]; }