Skip to content

Instantly share code, notes, and snippets.

@matt-dunn
Last active October 28, 2020 19:56
Show Gist options
  • Save matt-dunn/32c653b17fb7a31a294af18e6f39563c to your computer and use it in GitHub Desktop.
Save matt-dunn/32c653b17fb7a31a294af18e6f39563c to your computer and use it in GitHub Desktop.
/** !
* 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