import React, { createContext, useContext, useMemo, useReducer } from 'react' /** Takes objects that fully define a store and turns it into a more potent reducer */ export const useReducerStore = storeDef => { const { reducer = {}, thunks = {}, initialState } = storeDef const reducerFn = useMemo(() => makeReducerFn(reducer), [reducer]) const [state, dispatch] = useReducer(reducerFn, initialState) const _thunks = useMemo(() => bindThunks(thunks, dispatch), [ thunks, dispatch ]) return [state, _thunks] } /** A ReducerStore Factory that uses React.context * Used to make a Store accessible through multiple components */ export const createStore = storeDef => { const context = createContext(null) const Provider = ({ children }) => { const store = useReducerStore(storeDef) return {children} } const useStore = () => useContext(context) return { Provider, Consumer: context.Consumer, useStore } } /** Turn a reducer definition object to a function * @param {Object} reducer: Object definition of a reducer: {: (state, payload) => } * @returns {Function}: Proper reducer */ function makeReducerFn(reducer) { return (state, action) => { const { type, ...payload } = action if (typeof reducer[type] === 'undefined') return state const newState = reducer[type](state, payload) return newState } } /** Returns a set of actions that are bound to the store * @param {Object} actions: {: payload => ({dispatch, type}) => } */ function bindThunks(thunks, dispatch) { const _thunks = {} for (let name of Object.keys(thunks)) { const defaultType = camelToSnakeCase(name).toUpperCase() _thunks[name] = (...args) => thunks[name](...args)(patchDispatch(dispatch, defaultType)) } return _thunks } const camelToSnakeCase = str => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`) /** Returns a dispatch function that fills in a default type if none is given */ function patchDispatch(dispatch, type) { return action => { dispatch({ type, ...action }) } }