From 71ada438f594bbcedf8600bedf80673e862df9ab Mon Sep 17 00:00:00 2001 From: joe fleming Date: Fri, 22 Mar 2019 14:51:28 -0700 Subject: [PATCH] feat: add working store implementation --- .eslintrc | 4 +- index.js | 2 +- src/index.js | 3 -- src/index.mjs | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 6 deletions(-) delete mode 100644 src/index.js create mode 100644 src/index.mjs diff --git a/.eslintrc b/.eslintrc index 405f0f1..e1442ed 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,7 @@ { "parserOptions": { - "ecmaVersion": 8, - "sourceType": "script" + "ecmaVersion": 9, + "sourceType": "module" }, "extends": [ "airbnb", diff --git a/index.js b/index.js index 9298d9f..7f3227a 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,3 @@ -const mod = require('./src/index.js'); +const mod = require('./dist/index.js'); module.exports = mod; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 1084f7e..0000000 --- a/src/index.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function mod() { - // module code goes here -}; diff --git a/src/index.mjs b/src/index.mjs new file mode 100644 index 0000000..6280b99 --- /dev/null +++ b/src/index.mjs @@ -0,0 +1,121 @@ +/* eslint no-param-reassign: 0, react/destructuring-assignment: 0, react/no-multi-comp: 0 */ +import React, { createContext, useContext } from 'react'; +import PropTypes from 'prop-types'; +import { produce, original } from 'immer'; + +// keep track of all the stores +const stores = new Map(); + +// USAGE: createStore('myStore', { state, mutations, actions, getters }) +export function createStore(name, storeSpec) { + // don't duplicate stores + if (stores.has(name)) throw new Error(`Store '${name}' already exists`); + + const spec = { + state: {}, + mutations: {}, + actions: {}, + getters: {}, + ...storeSpec, + }; + + // create a new context for the store and add it to the map + const storeContext = createContext({ + state: spec.state, + commit: () => {}, + dispatch: () => {}, + getters: () => {}, + }); + + stores.set(name, { + context: storeContext, + provider: class StoreProvider extends React.Component { + static propTypes = { + children: PropTypes.node.isRequired, + }; + + state = { + state: spec.state, + commit: (n, ...args) => { + const mutation = spec.mutations[n]; + if (!mutation) throw new Error(`No such mutation: ${n}`); + this.setState( + produce(draft => { + mutation({ state: draft.state, original }, ...args); + }) + ); + }, + dispatch: async (n, ...args) => { + const action = spec.actions[n]; + if (!action) throw new Error(`No such action: ${n}`); + return produce(this.state.state, async draft => { + // NOTE: this does not change state, it's expected that + // actions use commit to update the state + return action({ state: draft, commit: this.state.commit }, ...args); + }); + }, + get: (n, ...args) => { + const getter = spec.getters[n]; + if (!getter) throw new Error(`No such getter: ${n}`); + return getter(this.state.state, ...args); + }, + }; + + render() { + return React.createElement( + storeContext.Provider, + { value: this.state }, + this.props.children + ); + } + }, + // TODO: keep track of the intial state, for resetting + // initialState: { ...spec.state }, + }); +} + +// TODO: method to build a larger state object made of stores +// composeStores({ +// todos: todoStore +// }); + +// hook to access a specific store and read/write methods +// USAGE: const { state, commit } = useStore('myStore') +export function useStore(name) { + if (!stores.has(name)) throw new Error(`Store does not exist: '${name}'`); + const store = stores.get(name); + return useContext(store.context); +} + +// provider to wrap your app or component +export class StateProvider extends React.Component { + static propTypes = { + children: PropTypes.node.isRequired, + names: PropTypes.string, + }; + + static defaultProps = { + names: '', + }; + + render() { + const { names, children } = this.props; + const namesArray = names.length ? names.split(',') : []; + + // provide all stores if none are specified + const provideAllStores = namesArray.length === 0; + + // build an array of all the store providers + const providers = provideAllStores + ? Array.from(stores).map(([, store]) => store.provider) + : namesArray.map(name => { + if (!stores.has(name)) throw new Error(`Store does not exist: '${name}'`); + return stores.get(name).provider; + }); + + // nest children inside all of the providers + return providers.reduce((acc, provider) => { + return React.createElement(provider, {}, acc); + }, children); + } +}