/* 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); } }