Skip to content

Instantly share code, notes, and snippets.

@jquense
Created November 1, 2018 18:39
Show Gist options
  • Save jquense/db418c25a43bdb89ddcb43d34fccd03c to your computer and use it in GitHub Desktop.
Save jquense/db418c25a43bdb89ddcb43d34fccd03c to your computer and use it in GitHub Desktop.

Revisions

  1. jquense created this gist Nov 1, 2018.
    105 changes: 105 additions & 0 deletions createStore.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,105 @@
    import React, { useReducer, useContext, useMemo } from 'react'
    import get from 'lodash/get'
    import invariant from 'invariant'

    const INIT = {
    type: `INIT${Math.random()
    .toString(36)
    .substring(7)
    .split('')
    .join('.')}`,
    }

    function compose(funcs) {
    if (funcs.length === 0) return arg => arg
    if (funcs.length === 1) return funcs[0]
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
    }

    export function bindActionCreators(actionCreators, dispatch) {
    if (typeof actionCreators === 'function')
    return (...args) => dispatch(actionCreators(...args))

    const boundActionCreators = {}
    for (const [key, actionCreator] of Object.entries(actionCreators)) {
    if (typeof actionCreator === 'function')
    boundActionCreators[key] = (...args) => dispatch(actionCreator(...args))
    }

    return boundActionCreators
    }

    export const combineReducers = reducers => {
    const entries = Object.entries(reducers)

    return (state = {}, action, parentPath = 'root') => {
    let hasChanged = false

    const nextState = {}
    for (let [key, reducer] of entries) {
    const path = `${parentPath}.${key}`
    const prevStateKey = state[key]
    const nextStateKey = reducer(prevStateKey, action, __DEV__ && path)
    invariant(
    nextStateKey !== undefined,
    action.type === INIT.type
    ? `Could not initialize state for reducer key: '${path}', return an initial state value or null for an empty state`
    : `Reducer at '${path}' did not return a state value, use null for an empty state`
    )
    nextState[key] = nextStateKey
    hasChanged = hasChanged || nextStateKey !== prevStateKey
    }

    return hasChanged ? nextState : state
    }
    }

    const applyMiddleware = (middlewares, dispatch) => {
    return compose(middlewares.map(m => m(dispatch)))(dispatch)
    }

    export default (reducers, initialState = {}, middleware) => {
    const StateContext = React.createContext({})
    const DispatchContext = React.createContext()

    const reducer = combineReducers(reducers)

    initialState = reducer(initialState, INIT)

    function useStoreDispatch() {
    return useContext(DispatchContext)
    }

    function useBoundActions(actionsToBind) {
    const dispatch = useStoreDispatch()
    return useMemo(() => bindActionCreators(actionsToBind, dispatch), [
    actionsToBind,
    ])
    }

    function useStoreState(key) {
    const state = useContext(StateContext)
    return key ? get(state, key) : state
    }

    function Provider({ children }) {
    const [state, baseDispatch] = useReducer(reducer, initialState)

    const dispatch = useMemo(() => applyMiddleware(middleware, baseDispatch), [
    baseDispatch,
    ])

    return (
    <DispatchContext.Provider value={dispatch}>
    <StateContext.Provider value={state}>{children}</StateContext.Provider>
    </DispatchContext.Provider>
    )
    }

    return {
    useBoundActions,
    useStoreDispatch,
    useStoreState,
    Provider,
    }
    }