-
-
Save acdlite/9f1b5883d132ad242323 to your computer and use it in GitHub Desktop.
| /** | |
| * 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 }); | |
| } | |
| } |
| /** | |
| * 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> | |
| ); | |
| } | |
| } |
Actually I guess "higher-order store" is technically incorrect, since it's not a store that returns another store. Oh well :D
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;
Two key differences from Redux:
shouldComponentUpdate()is for.