hyperappish

1.6.0 • Public • Published

hyperappish

npm Build Status

A minimal, zero dependency (!), hyperapp-like, wired action, state handling-thingy that works with plain react components.

npm install hyperappish

How does it work?

Create a state tree and a corresponding operations object that modify each part of it.

const state = {
  counter: {
    n: 42
  }
};
 
const ops = {
  counter: {
    increment: state => ({ n: state.n + 1 })
  }
};

Actions are automatically bound to the part of the state that matches the key under which they are defined in the operations object (much like in hyperapp). They are called with this part of the state automatically when invoked.

E.g. the increment action will get passed the counter part of the state when it is invoked, as it resides under the counter key of the ops object.

Call mount with the state and operations to connect them.

import { mount } from "hyperappish";
const { run, actions } = mount(state, ops);

Call run with a function to render your application. This function is passed the state every time it is changed.

import React from "react";
import { render } from "react-dom";
 
const el = document.querySelector(".app");
run(state =>
  render(<button onClick={ () => actions.counter.increment() }>
           {state.counter}++
         </button>, el));

This renders a button with the value 42++ that when clicked will increment its value, over and over, ad infinitum.

Run it or play with it in codesandbox.

Or head over to torgeir/hyperappish-example for a complete example.

Promises, observables and middleware

This larger, contrieved example shows how to

  • Compose promises in actions
  • Return state directly from actions, even from promises
  • Express advanced async flows declaratively with e.g. observables from rxjs
  • Use middlewares to extend the default behavior, e.g. for logging actions or state after each change
import { mount } from "hyperappish";
import React from "react";
import ReactDOM from "react-dom";
import Rx from "rxjs/Rx";
 
const wait = s => new Promise(resolve => setTimeout(resolve, s * 1000));
 
const state = {
  incrementer: {
    incrementing: false,
    n: 0
  },
  selection: {
    user: null
  },
  users: {
    list: []
  }
};
 
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 =>
      wait(2)
        .then(_ => fetch("https://jsonplaceholder.typicode.com/users"))
        .then(res => res.json())
        .then(users => users.map(({ id, name }) => ({ id, name })))
        .then(list => ({ ...state, list }))
  }
};
 
const { run, actions, middleware, getState } = mount(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 = v => v }) => (
  <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 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} />
    <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());
 
const el = document.querySelector(".app");
run(state => ReactDOM.render(<App {...state} />, el), [
  middlewares.logActions,
  middleware.callAction
  middlewares.promise,
  middlewares.observable(incrementer),
  middlewares.logState(getState)
]);

Run it or play with it in codesandbox.

Middlewares

If you choose to provide your own middlewares, remember to add middleware.callAction (returned from mount) early in middleware list. This is the middleware that hyperappish use to actually call the action function with its corresponding state before returning control to the other middlewares.

You can override the default middleware by passing a list of middlewares as the second parameter to run(). These receive each action and the next middleware in the list, and may choose how to proceed.

Here are a couple of useful ones:

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(v => v));
    return (action, next) => {
      const result = next(action);
      action$.next(action);
      return result;
    };
  },
 
  logActions: (action, next) => (console.log("action", action), next(action)),
 
  logState: getState => (action, next) => {
    const res = next(action);
    console.log("state", getState()));
    return res;
  }
};

Contributions?

Most welcome! Hit me up with a PR or an issue.

Package Sidebar

Install

npm i hyperappish

Weekly Downloads

1

Version

1.6.0

License

MIT

Unpacked Size

19.5 kB

Total Files

8

Last publish

Collaborators

  • torgeir