store.js 1.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. import { useState, useEffect } from "react";
  2. const createObservable = initial => {
  3. let _val = initial;
  4. let _listeners = [];
  5. return {
  6. _get: () => _val,
  7. _set: newValue => {
  8. const oldValue = _val;
  9. _val = newValue;
  10. _listeners
  11. .filter(({ equality }) => !equality(oldValue, newValue))
  12. .forEach(({ callback }) => callback(newValue));
  13. },
  14. _sub: (callback, equality) => {
  15. _listeners.push({ callback, equality });
  16. return () => {
  17. _listeners = _listeners.filter(ln => ln.callback !== callback);
  18. }
  19. }
  20. }
  21. }
  22. const nameToHookName = name => "use" + name.chatAt(0).toUpperCase() + name.slice(1);
  23. const shallowEq = (x, y) => x === y;
  24. const createHook = obs => {
  25. // allow linters to pick this up as a hook
  26. const useObservable = (equality = shallowEq) => {
  27. const [val, setVal] = useState(obs._get());
  28. useEffect(() => obs._sub(setVal, equality), [equality]);
  29. return val;
  30. }
  31. return useObservable;
  32. }
  33. export const createStore = (initial, actions = {}) => {
  34. const store = {}; // maps keys to observables
  35. const hooks = {};
  36. const mergeState = newState => {
  37. Object.entries(newState).forEach(([key, newValue]) => {
  38. const obs = store[key];
  39. if (obs) {
  40. obs._set(newValue);
  41. } else {
  42. const newObs = createObservable(newValue);
  43. store[key] = newObs;
  44. hooks[nameToHookName(key)] = createHook(newObs);
  45. }
  46. });
  47. };
  48. mergeState(initial);
  49. const dispatch = Object.fromEntries(Object.entries(actions).map(([name, action]) => [name, (...args) => mergeState(action(...args))]));
  50. const selector = Object.fromEntries(Object.entries(store).map(([key, obs]) => [key, obs._get]));
  51. return [hooks, dispatch, selector];
  52. }