-
-
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.