import React, { createContext, useContext, useMemo, useReducer } from 'react' /** Takes options that fully define a store and turns it into a more potent reducer */ export const usePotentReducer = options => { const { reducer = {}, thunks = {}, initialState } = options const { onUpdate, logging } = options const reducerFn = useMemo( () => makeReducerFn(reducer, { onUpdate, logging }), [reducer, onUpdate, logging] ) 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 = options => { const context = createContext(null) const Provider = ({ children }) => { const store = usePotentReducer(options) return React.createElement(context.Provider, { value: store }, children) } const useStore = () => useContext(context) const useSelector = selector => selector(useContext(context)[0]) return { Provider, Consumer: context.Consumer, useStore, useSelector } } /** 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, { onUpdate, logging }) { const noop = state => state return (prevState, action) => { const { type, ...payload } = action const reducerBranch = reducer[type] || reducer['default'] || noop const nextState = reducerBranch(prevState, payload) const stats = { prevState, nextState, action } typeof onUpdate === 'function' && onUpdate(stats) if (logging) logger(stats) return nextState } } /** 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 }) } } /** Log action inspired by redux-logger */ const logger = ({ prevState, action, nextState }) => { const css = color => `color: ${color}; font-weight: bold;` console.groupCollapsed(`%c action`, 'color: #9E9E9E', action.type) console.log('%c prevState ', css('#9E9E9E'), prevState) console.log('%c action ', css('#03A9F4'), action) console.log('%c nextState ', css('#4CAF50'), nextState) console.groupEnd() }