2020-02-01 12:18:32 +01:00
|
|
|
import React, { createContext, useContext, useMemo, useReducer } from 'react'
|
|
|
|
|
2020-02-01 17:21:18 +01:00
|
|
|
/** Takes options that fully define a store and turns it into a more potent reducer */
|
2020-02-01 15:53:44 +01:00
|
|
|
export const usePotentReducer = options => {
|
|
|
|
const { reducer = {}, thunks = {}, initialState } = options
|
|
|
|
const { onUpdate, logging } = options
|
|
|
|
const reducerFn = useMemo(
|
|
|
|
() => makeReducerFn(reducer, { onUpdate, logging }),
|
|
|
|
[reducer, onUpdate, logging]
|
|
|
|
)
|
2020-02-01 12:18:32 +01:00
|
|
|
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
|
|
|
|
*/
|
2020-02-01 15:53:44 +01:00
|
|
|
export const createStore = options => {
|
2020-02-01 12:18:32 +01:00
|
|
|
const context = createContext(null)
|
2020-02-01 18:06:29 +01:00
|
|
|
const { Consumer } = context
|
2020-02-01 12:18:32 +01:00
|
|
|
const Provider = ({ children }) => {
|
2020-02-01 15:53:44 +01:00
|
|
|
const store = usePotentReducer(options)
|
2020-02-01 14:51:53 +01:00
|
|
|
return React.createElement(context.Provider, { value: store }, children)
|
2020-02-01 12:18:32 +01:00
|
|
|
}
|
|
|
|
const useStore = () => useContext(context)
|
2020-02-01 17:21:08 +01:00
|
|
|
const useSelector = selector => selector(useContext(context)[0])
|
2020-02-01 18:06:29 +01:00
|
|
|
const useDispatch = () => useContext(context)[1]
|
|
|
|
return { Provider, Consumer, useStore, useSelector, useDispatch }
|
2020-02-01 12:18:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Turn a reducer definition object to a function
|
|
|
|
* @param {Object} reducer: Object definition of a reducer: {<type>: (state, payload) => <newState>}
|
|
|
|
* @returns {Function}: Proper reducer
|
|
|
|
*/
|
2020-02-01 15:53:44 +01:00
|
|
|
function makeReducerFn(reducer, { onUpdate, logging }) {
|
|
|
|
const noop = state => state
|
|
|
|
return (prevState, action) => {
|
2020-02-01 18:09:03 +01:00
|
|
|
const { type } = action
|
2020-02-01 15:53:44 +01:00
|
|
|
const reducerBranch = reducer[type] || reducer['default'] || noop
|
2020-02-01 18:09:03 +01:00
|
|
|
const nextState = reducerBranch(prevState, action)
|
2020-02-01 15:53:44 +01:00
|
|
|
const stats = { prevState, nextState, action }
|
|
|
|
typeof onUpdate === 'function' && onUpdate(stats)
|
|
|
|
if (logging) logger(stats)
|
|
|
|
return nextState
|
2020-02-01 12:18:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Returns a set of actions that are bound to the store
|
|
|
|
* @param {Object} actions: {<name>: 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 })
|
|
|
|
}
|
|
|
|
}
|
2020-02-01 15:53:44 +01:00
|
|
|
|
|
|
|
/** 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()
|
|
|
|
}
|