rsvp-reader/lib/potent-reducer.js

114 lines
4.1 KiB
JavaScript
Raw Normal View History

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
2020-02-02 14:31:48 +01:00
const { extState, extDispatch } = options
2020-02-01 15:53:44 +01:00
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)
2020-02-02 14:31:48 +01:00
const _thunks = useMemo(() => bindThunks(thunks, extDispatch || dispatch), [
2020-02-01 12:18:32 +01:00
thunks,
2020-02-02 14:31:48 +01:00
extDispatch
2020-02-01 12:18:32 +01:00
])
2020-02-02 14:31:48 +01:00
return [extState || state, _thunks]
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>}
2020-02-01 18:57:58 +01:00
* @param {Object} options
2020-02-01 12:18:32 +01:00
* @returns {Function}: Proper reducer
*/
2020-02-02 14:31:48 +01:00
export function makeReducerFn(reducer, options = {}) {
const { onUpdate, logging } = options
2020-02-01 15:53:44 +01:00
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
}
}
2020-02-01 18:57:58 +01:00
/** Produces a set of actions that are bound to a specific store
* The thunks object can contain action creators and/or thunks.
* - Action creators: args => action
* are dispatched directly when called
* - Thunks args => dispatch => dispatch(action)
* are called with dispatch as first argument.
* The dispatch function will be patched to insert an action type if not given.
* This action type will be the SNAKE_CASE'd action name
* @param {Object} thunks
* @param {Function} dispatch
* @returns {object}
2020-02-01 12:18:32 +01:00
*/
function bindThunks(thunks, dispatch) {
2020-02-01 18:57:58 +01:00
const boundThunks = {}
2020-02-01 12:18:32 +01:00
for (let name of Object.keys(thunks)) {
const defaultType = camelToSnakeCase(name).toUpperCase()
2020-02-01 18:57:58 +01:00
const patchedDispatch = patchDispatch(dispatch, defaultType)
boundThunks[name] = (...args) => {
const thunk = thunks[name](...args)
if (typeof thunk !== 'function') patchedDispatch(thunk)
else thunk(patchedDispatch)
}
2020-02-01 12:18:32 +01:00
}
2020-02-01 18:57:58 +01:00
return boundThunks
2020-02-01 12:18:32 +01:00
}
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
2020-02-01 21:21:01 +01:00
/** A ReducerStore Factory that uses React.context
* Used to make a Store accessible through multiple components
*/
2020-02-02 14:31:48 +01:00
export const createStore = initialOptions => {
2020-02-01 21:21:01 +01:00
const context = createContext(null)
const { Consumer } = context
2020-02-02 14:31:48 +01:00
const Provider = ({ children, options }) => {
const store = usePotentReducer({ ...initialOptions, ...options })
2020-02-01 21:21:01 +01:00
return React.createElement(context.Provider, { value: store }, children)
}
const useStore = () => useContext(context)
const useSelector = selector => {
const val = selector(useContext(context)[0])
2020-02-02 14:31:48 +01:00
initialOptions.warnOnUndefinedSelect && assertSelectedExists(val)
2020-02-01 21:21:01 +01:00
return val
}
const useDispatch = () => useContext(context)[1]
return { Provider, Consumer, useStore, useSelector, useDispatch }
}
function assertSelectedExists(value) {
2020-02-01 21:23:56 +01:00
if (typeof value !== 'undefined') return
2020-02-01 21:21:01 +01:00
console.warn(`A selector returned undefined.
This indicates that you tried to access a state property that does not exist.
To turn these messages off use {"warnOnUndefinedSelect": false} in options`)
console.groupCollapsed('Trace')
console.trace()
console.groupEnd()
}
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()
}