Skip to content

Instantly share code, notes, and snippets.

@torgeir torgeir/hyperappish.js
Last active Apr 21, 2018

Embed
What would you like to do?
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

This comment has been minimized.

Copy link
Owner Author

commented Mar 24, 2018

@torgeir

This comment has been minimized.

Copy link
Owner Author

commented Apr 21, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.