Skip to content

Instantly share code, notes, and snippets.

@torgeir
Last active April 21, 2018 20:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save torgeir/531e37264e4bd61551f755e5be30297d to your computer and use it in GitHub Desktop.
Save torgeir/531e37264e4bd61551f755e5be30297d to your computer and use it in GitHub Desktop.
A minimal, hyperapp-like, wired action, state handling thingy that works with plain react components
import React from "react";
import ReactDOM from "react-dom";
import Rx from "rxjs/Rx";
// hyperappish
const identity = v => v;
const doto = (o, fn) => (fn(o), o);
const getIn = (obj, [k, ...ks]) => (ks.length == 0) ? obj[k] : getIn(obj[k], ks);
const setIn = (obj, [k, ...ks], val) => (ks.length == 0) ? (obj[k] = val) && val : setIn(obj[k], ks, val);
const middleware = ([fn, next, ...fns]) => action => fn(action, next ? middleware([next, ...fns]) : identity);
const setup = function (state, ops) {
const wrappedState = { state };
let el, renderer, middlewares;
const renderAfter = fn => (...args) =>
doto(fn(...args),
_ => setTimeout(_ => renderer && ReactDOM.render(renderer(wrappedState.state), el), 0));
const proxy = (o, path) => new Proxy(
getIn(o, path), // makes {...state} work
{
get: (_, name) => getIn(o, path.concat(name)),
set: renderAfter((_, name, value) => setIn(o, path.concat(name), value))
});
const createDispatchWithState = (fn, field, state, path) => function (...args) {
const result = fn(...args, proxy({...state}, path), actions);
const handlers = middleware(middlewares.concat(renderAfter(action => setIn(state, path, action.result))));
const type = path.slice(1).concat(field).join(".");
handlers({ type, result });
};
const patch = (ops, state, path) =>
Object.keys(ops)
.reduce((acc, field) =>
doto(acc, acc =>
acc[field] = (typeof ops[field] == 'function')
? createDispatchWithState(ops[field], field, state, path)
: patch(ops[field], state, path.concat(field))),
{});
const actions = patch(ops, wrappedState, ['state']);
return {
actions,
render: renderAfter((_el, _renderer, _middlewares = []) => {
el = _el;
renderer = _renderer;
middlewares = _middlewares;
})
};
};
const middlewares = {
promise: (action, next) =>
(typeof action.result.then == 'function')
? action.result.then(result => next({ ...action, result }))
: next(action),
observable: (...epics) => {
const action$ = new Rx.Subject();
epics.map(epic => epic(action$).subscribe(identity));
return (action, next) => {
const result = next(action);
action$.next(action);
return result;
};
},
logActions: (action, next) =>
(console.log('action', action), next(action)),
logState: (action, next) =>
doto(next(action), _ => console.log('state', state))
};
// an app
const state = {
incrementer: {
incrementing: false,
n: 0
},
selection: {
user: null
},
users: {
list: []
},
counter: {
n: 0
}
};
const ops = {
incrementer: {
start: (state) => ({ ...state, incrementing: true }),
increment: (state) => ({ ...state, n: state.n + 1 }),
stop: (state) => ({ ...state, incrementing: false })
},
selection: {
select: (user) => ({ user }),
remove: () => ({ user: null })
},
users: {
list: (state) =>
fetch("https://jsonplaceholder.typicode.com/users")
.then(res => res.json())
.then(users => users.map(({id, name}) => ({id, name})))
.then(list => ({ ...state, list }))
},
counter: {
up: (v, state) =>
new Promise(resolve => setTimeout(_ => resolve({ ...state, n: state.n + v }), 1000)),
down: (state) => ({ ...state, n: state.n - 1 })
}
};
const {render, actions} = setup(state, ops);
const SelectedUser = ({user}) => <div>
<h2>Selected user</h2>
<span>{ user.name } <button onClick={ () => actions.selection.remove() }>x</button></span>
</div>;
const User = ({user, onClick = identity}) => <span onClick={ onClick } style={{ cursor: 'pointer' }}>
{ user.name } ({ user.id })
</span>;
const Users = ({list}) => {
if (!list.length) {
actions.users.list();
return <span>Loading..</span>;
}
return <div>
<h2>Users</h2>
{ list.map(user =>
<div key={user.id}>
<User user={user} onClick={ () => actions.selection.select(user) } />
</div>) }
</div>;
};
const Counter = ({n}) => <div>
<h2>Counter</h2>
<button onClick={ () => actions.counter.up(2) }>delayed 2x up</button>
<span> {n} </span>
<button onClick={ () => actions.counter.down() }>1x down</button>
</div>;
const Incrementer = ({n}) => <div>
<h2>Incrementer</h2>
Incrementing: { n }
<button onClick={ () => actions.incrementer.start() }>start</button>
<button onClick={ () => actions.incrementer.stop() }>stop</button>
</div>;
const App = ({ selection, users, counter, incrementer }) => <div>
{ selection.user && <SelectedUser user={selection.user} /> }
<Users { ...users } />
<Counter { ...counter } />
<Incrementer { ...incrementer } />
</div>;
const incrementer = action$ =>
action$
.filter(action => action.type == 'incrementer.start')
.switchMap(() =>
Rx.Observable
.interval(100)
.takeUntil(action$.filter(action => action.type == 'incrementer.stop')))
.map(() => actions.incrementer.increment());
render(
document.querySelector('.app'),
state => <App { ...state } />,
[
middlewares.promise,
middlewares.observable(incrementer),
middlewares.logActions,
middlewares.logState,
]);
@torgeir
Copy link
Author

torgeir commented Mar 24, 2018

@torgeir
Copy link
Author

torgeir commented Apr 21, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment