Last active
October 28, 2020 19:56
-
-
Save matt-dunn/32c653b17fb7a31a294af18e6f39563c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** ! | |
* Copyright (c) 2020, Matt Dunn | |
* | |
* @author Matt Dunn (https://matt-dunn.github.io/) | |
* @licence MIT | |
*/ | |
/** | |
* Example of a dependency free flux style state implementation | |
* See https://github.com/matt-dunn/state/blob/master/app/index.tsx for a typescript | |
* version with middleware chain | |
*/ | |
import React, { | |
useContext, useEffect, useMemo, useState, | |
} from "react"; | |
import PropTypes from "prop-types"; | |
import ReactDOM from "react-dom"; | |
// # Store ############################################### | |
const getStore = (initialState = {}, reducers = {}) => { | |
let state = initialState; | |
let callbacks = []; | |
return { | |
dispatch: (action) => { | |
const newState = Object.keys(reducers).reduce((currentState, key) => { | |
const updatedState = reducers[key](currentState[key], action); | |
if (updatedState !== currentState[key]) { | |
return { | |
...currentState, | |
[key]: updatedState, | |
}; | |
} | |
return currentState; | |
}, state); | |
if (newState !== state) { | |
callbacks.forEach(callback => callback(newState, state)); | |
state = newState; | |
} | |
}, | |
getState: () => state, | |
register: (cb) => { | |
callbacks = [...callbacks, cb]; | |
}, | |
unregister: (cb) => { | |
callbacks = callbacks.filter(callback => callback !== cb); | |
}, | |
}; | |
}; | |
// # State Utilities ############################################### | |
const typeSymbol = Symbol("type"); | |
const createAction = (type, payloadCreator = undefined) => { | |
const action = (...args) => ({ | |
type, | |
payload: payloadCreator && payloadCreator(...args), | |
}); | |
action[typeSymbol] = type; | |
return action; | |
}; | |
const getType = actionCreator => actionCreator[typeSymbol]; | |
const createReducer = reducers => ((state, action) => { | |
const reducer = reducers[action.type]; | |
return (reducer && reducer(state, action)) || state; | |
}); | |
const storeContext = React.createContext(undefined); | |
const StoreProvider = storeContext.Provider; | |
// # React Connector ############################################### | |
const connect = (mapStateToProps, mapDispatchToProps) => WrappedComponent => (props) => { | |
const store = useContext(storeContext); | |
const [state, setState] = useState(store.getState()); | |
useEffect(() => { | |
store.register(setState); | |
return () => { | |
store.unregister(setState); | |
}; | |
}, [store]); | |
const stateProps = useMemo(() => mapStateToProps(state), [state]); | |
const dispatchProps = useMemo(() => Object.keys(mapDispatchToProps).reduce((componentProps, key) => ({ | |
[key]: (...args) => store.dispatch(mapDispatchToProps[key](...args)), | |
...componentProps, | |
}), {}), [store]); | |
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />; | |
}; | |
// # Test Code ############################################### | |
// - Test Components --------------------------------------------- | |
const Counter = ({ count, onIncrease, onDecrease }) => { | |
const handleIncrease = () => onIncrease(10); | |
const handleDecrease = () => onDecrease(3); | |
return ( | |
<> | |
<p> | |
The count is | |
{" "} | |
{count} | |
. | |
</p> | |
<button | |
type="button" | |
onClick={handleDecrease} | |
> | |
Decrement | |
</button> | |
<button | |
type="button" | |
onClick={handleIncrease} | |
> | |
Increment | |
</button> | |
</> | |
); | |
}; | |
Counter.propTypes = { | |
count: PropTypes.number.isRequired, | |
onIncrease: PropTypes.func.isRequired, | |
onDecrease: PropTypes.func.isRequired, | |
}; | |
const CounterApp = ({ | |
count, increment, decrement, reset, children, | |
}) => { | |
const handleIncrease = value => increment(value); | |
const handleDecrease = value => decrement(value); | |
const handleReset = () => reset(); | |
return ( | |
<main> | |
{children} | |
<Counter count={count} onIncrease={handleIncrease} onDecrease={handleDecrease} /> | |
<button | |
type="button" | |
onClick={handleReset} | |
> | |
Reset | |
</button> | |
</main> | |
); | |
}; | |
CounterApp.propTypes = { | |
count: PropTypes.number.isRequired, | |
increment: PropTypes.func.isRequired, | |
decrement: PropTypes.func.isRequired, | |
reset: PropTypes.func.isRequired, | |
children: PropTypes.element, | |
}; | |
// - Test Actions --------------------------------------------- | |
const increment = createAction("COUNTER:INCREMENT", (value = 1) => value); | |
const decrement = createAction("COUNTER:DECREMENT", (value = 1) => value); | |
const reset = createAction("COUNTER:RESET"); | |
// - Test Reducers --------------------------------------------- | |
const counterReducers = createReducer({ | |
[getType(increment)]: (state, { payload }) => ({ count: state.count + payload }), | |
[getType(decrement)]: (state, { payload }) => ({ count: state.count - payload }), | |
[getType(reset)]: () => ({ count: 0 }), | |
}); | |
// - Test Connected Component (container) --------------------------------------------- | |
const ConnectedCounter = connect( | |
state => ({ | |
count: state.counter.count, | |
}), | |
{ | |
onIncrease: increment, | |
onDecrease: decrement, | |
}, | |
)(Counter); | |
const ConnectedCounterApp = connect( | |
state => ({ | |
count: state.counter.count, | |
}), | |
{ | |
increment, | |
decrement, | |
reset, | |
}, | |
)(CounterApp); | |
// - Test Setup state, root reducer, store --------------------------------------------- | |
const initialState = { | |
counter: { | |
count: 10, | |
}, | |
}; | |
const rootReducer = { | |
counter: counterReducers, | |
}; | |
const store = getStore(initialState, rootReducer); | |
// - Test App --------------------------------------------- | |
ReactDOM.render(( | |
<StoreProvider value={store}> | |
<ConnectedCounterApp> | |
<ConnectedCounter /> | |
</ConnectedCounterApp> | |
</StoreProvider> | |
), document.getElementById("app")); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment