Last active
October 7, 2021 17:19
-
-
Save acdlite/9f1b5883d132ad242323 to your computer and use it in GitHub Desktop.
A Redux-like Flux implementation in <75 lines of code
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
/** | |
* Basic proof of concept. | |
* - Hot reloadable | |
* - Stateless stores | |
* - Stores and action creators interoperable with Redux. | |
*/ | |
import React, { Component } from 'react'; | |
export default function dispatch(store, atom, action) { | |
return store(atom, action); | |
} | |
export class Dispatcher extends Component { | |
static propTypes = { | |
store: React.PropTypes.func.isRequired | |
}; | |
static childContextTypes = { | |
dispatch: React.PropTypes.func, | |
atom: React.PropTypes.any | |
}; | |
getChildContext() { | |
return { | |
atom: this.state.atom, | |
dispatch: this.dispatch.bind(this) | |
}; | |
} | |
constructor(props, context) { | |
super(props, context); | |
this.state = { atom: dispatch(props.store, undefined, {}) }; | |
} | |
dispatch(payload) { | |
this.setState(prevState => ({ | |
atom: dispatch(this.props.store, prevState.atom, payload) | |
})); | |
} | |
render() { | |
return typeof this.props.children === 'function' | |
? this.props.children(this.state.atom) | |
: this.props.children; | |
} | |
} | |
export class Injector extends Component { | |
static contextTypes = { | |
dispatch: React.PropTypes.func.isRequired, | |
atom: React.PropTypes.any | |
}; | |
static propTypes = { | |
actions: React.PropTypes.object | |
}; | |
performAction(actionCreator, ...args) { | |
const { dispatch } = this.context; | |
const payload = actionCreator(...args); | |
return typeof payload === 'function' | |
? payload(dispatch) | |
: dispatch(payload); | |
}; | |
render() { | |
const { dispatch, atom } = this.context; | |
const { actions: _actions } = this.props; | |
const actions = Object.keys(_actions).reduce((result, key) => { | |
result[key] = this.performAction.bind(this, _actions[key]); | |
return result; | |
}, {}); | |
return this.props.children({ actions, atom }); | |
} | |
} |
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
/** | |
* Example usage | |
* | |
* Based on Redux's counter example | |
* https://github.com/gaearon/redux/tree/master/examples/counter | |
*/ | |
import React, { Component, PropTypes } from 'react'; | |
import { Dispatcher, Injector } from '../'; | |
const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; | |
const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; | |
function counterStore(counter = 0, action) { | |
switch (action.type) { | |
case INCREMENT_COUNTER: | |
return counter + 1; | |
case DECREMENT_COUNTER: | |
return counter - 10; | |
default: | |
return counter; | |
} | |
} | |
function increment() { | |
return { | |
type: INCREMENT_COUNTER | |
}; | |
} | |
function decrement() { | |
return { | |
type: DECREMENT_COUNTER | |
}; | |
} | |
export default class CounterApp { | |
render() { | |
return ( | |
<Dispatcher | |
// Instead of specifying an object of keys mapped to stores, just use a | |
// higher-order store! | |
store={(state = {}, action) => ({ | |
counter: counterStore(state.counter, action) | |
})} | |
> | |
{() => ( | |
<Injector actions={{ increment, decrement }}> | |
{({ actions, atom }) => ( | |
<Counter | |
increment={actions.increment} | |
decrement={actions.decrement} | |
counter={atom.counter} | |
/> | |
)} | |
</Injector> | |
)} | |
</Dispatcher> | |
); | |
} | |
} | |
class Counter { | |
static propTypes = { | |
increment: PropTypes.func.isRequired, | |
decrement: PropTypes.func.isRequired, | |
counter: PropTypes.number.isRequired | |
}; | |
render() { | |
const { increment, decrement, counter } = this.props; | |
return ( | |
<p> | |
Clicked: {counter} times | |
{' '} | |
<button onClick={increment}>+</button> | |
{' '} | |
<button onClick={decrement}>-</button> | |
</p> | |
); | |
} | |
} |
This makes my weekend. 👍
Say we provide compose
that takes an array of a map of Stores and combines them into a single Store. Now that would be a higher-order Store :-)
No subscribing to individual stores; the entire atom is sent on each change.
Yeah, I suppose we could do that. But technically I'd still put redux.observe
in context instead of atom
because context
doesn't work well with shouldComponentUpdate
currently.
@acdlite I want to share with you this state management solution. Works well with hooks and it has an amazing typescript support. It is also a single state tree, check it out: https://overmindjs.org
@gaearon @acdlite
This is a draft and not completed:
demo: https://codesandbox.io/s/atomic-redux-q9prt
Atomic Redux:
import createAtom from "./atomic-redux";
const incomeAtom = createAtom({
initialState: 0,
actions: {
change: (state, payload) => payload
}
});
const taxAtom = createAtom({
initialState: 0.1,
subscribes: [incomeAtom],
so: (income) => {
if (income < 1000) return 0.1;
if (income < 2000) return 0.15;
if (income < 3000) return 0.2;
if (income < 4000) return 0.25;
return 0.3;
}
});
export default function App() {
const [income, incomeActions] = incomeAtom.useHook();
const [tax] = taxAtom.useHook();
const handleChange = (event) => incomeActions.change(event.target.value);
return (
<div>
<input value={income} onChange={handleChange} />
<br />
Your Income is: {income}
<br />
Your Tax is: {tax}
<br />
</div>
);
}
And inside /atomic-redux.js
import { useEffect, useState } from "react";
import { createStore } from "redux";
const PRIVATE_UPDATER = Symbol("PRIVATE_UPDATER");
const noop = () => {};
function createAtom({
initialState,
actions = {},
subscribes = [],
so = noop
}) {
actions[PRIVATE_UPDATER] = (_, payload) => payload;
let {
getState,
dispatch,
subscribe
} = createStore((state = initialState, { type, payload }) =>
(actions[type] || (() => state))(state, payload)
);
subscribes.forEach((atom) =>
atom.subscriber({
dispatch,
so
})
);
const useHook = () => {
const [state, setState] = useState(getState());
useEffect(() => {
const unsubscribe = subscribe(() => setState(getState()));
return unsubscribe;
}, []);
return [
state,
Object.keys(actions).reduce(
(acc, action) => ({
...acc,
[action]: (payload) => dispatch({ type: action, payload })
}),
{}
)
];
};
const subscriber = ({ dispatch, so }) =>
subscribe(() =>
dispatch({ type: PRIVATE_UPDATER, payload: so(getState()) })
);
return { useHook, subscriber };
}
export default createAtom;
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Actually I guess "higher-order store" is technically incorrect, since it's not a store that returns another store. Oh well :D