A light-weight type-safe Elm-like alternative for Redux ecosystem, inspired by hyperapp and Elmish
A light-weight Elm-like alternative for Redux ecosystem, inspired by Hyperapp and Elmish.
yarn add hydux # or npm i hydux
This is an experimental dependency injection API for actions inspired by react-hooks, totally downward compatible, with this we don't need curring to inject state and actions or manually markup types for the return value any more!
yarn add [email protected]^v0.5.8
import { inject } from 'hydux'export default { init: () => ({ count: 1 }), actions: { down() { let { state, actions, setState, Cmd } = inject() setState({ count: state.count - 1 }) Cmd.addSub(_ => _.log('down -1')) actions.up() }, up() { let { state, actions } = inject() return { count: state.count + 1 } }, log(msg) { console.log(msg) } }, view: (state, actions) => { return (
) } }{state.count}
– +
Let's say we got a counter, like this.
// Counter.js export default { init: () => ({ count: 1 }), actions: { down: () => state => ({ count: state.count - 1 }), up: () => state => ({ count: state.count + 1 }) }, view: (state: State, actions: Actions) =>}{state.count}
– +
Then we can compose it in Elm way, you can easily reuse your components.
import _app from 'hydux' import withUltradom, { h, React } from 'hydux/lib/enhancers/ultradom-render' import Counter from './counter'// use built-in 1kb ultradom to render the view. let app = withUltradom()(_app)
const actions = { counter1: Counter.actions, counter2: Counter.actions, }
const state = { counter1: Counter.init(), counter2: Counter.init(), }
const view = ( state: State, actions: Actions, ) =>
Counter1:
{Counter.view(state.counter1, actions.counter1)}Counter2:
{Counter.view(state.counter2, actions.counter2)}export default app({ init: () => state, actions, view, })
You can init the state of your app via plain object, or with side effects, like fetch remote data.
import * as Hydux from 'hydux'const { Cmd } = Hydux
export function init() { return { state: { // pojo state count: 1, }, cmd: Cmd.ofSub( // update your state via side effects. _ => fetch('https://your.server/init/count') //
_
is the real actions, don't confuse with the plain objectactions
that we created below, calling functions from plain object won't trigger state update! .then(res => res.json()) .then(count => _.setCount(count)) ) } }export const actions = { setCount: n => (state, actions) => { return { count: n } } }
If we want to init a child component with init command, we need to map it to the sub level via lambda function, just like type lifting in Elm. ```ts // App.tsx import { React } from 'hydux-react' import * as Hydux from 'hydux' import * as Counter from 'Counter' const Cmd = Hydux.Cmd
export const init = () => { const counter1 = Counter.init() const counter2 = Counter.init() return { state: { counter1: counter1.state, counter2: counter2.state, }, cmd: Cmd.batch( Cmd.map((: Actions) => _.counter1, counter1.cmd), // Map counter1's init command to parent component Cmd.map((: Actions) => _.counter2, counter2.cmd), // Map counter2's init command to parent component Cmd.ofSub( _ => // some other commands of App ) ) } }
export const actions = { counter1: Counter.actions, counter2: Counter.actions, // ... other actions }
export const view = (state: State, actions: Actions) => (
export type Actions = typeof actions export type State = ReturnType['state'] ```
This might be too much boilerplate code, but hey, we provide a type-friendly helper function! See:
// Combine all sub components's init/actions/views, auto map init commands. const subComps = Hydux.combine({ counter1: [Counter, Counter.init()], counter2: [Counter, Counter.init()], }) export const init2 = () => { return { state: { ...subComps.state, // other state }, cmd: Cmd.batch( subComps.cmd, // other commands ) } }export const actions = { ...subComps.actions, // ... other actions }
export const view = (state: State, actions: Actions) => (
) Counter1:
{subComps.render('counter1', state, actions)} // euqal to: // {subComps.views.counter1(state.counter1, actions.counter1)} // .render('', ...) won't not work with custom views that not match(state, actions) => any
or(props) => any
signature // So we still need.views.counter1(...args)
in this case.Counter2:
{subComps.render('counter2', state, actions)}
This library also implemented a Elm-like side effects manager, you can simple return a record with state, cmd in your action e.g.
import app, { Cmd } from 'hydux'function upLater(n) { return new Promise(resolve => setTimeout(() => resolve(n + 10), 1000)) } app({ init: () => ({ count: 1}), actions: { down: () => state => ({ count: state.count - 1 }), up: () => state => ({ count: state.count + 1 }), upN: n => state => ({ count: state.count + n }), upLater: n => ( state, actions/* actions of same level / ) => ({ state, // don't change the state, won't trigger view update cmd: Cmd.ofPromise( upLater / a function with single parameter and return a promise /, n / the parameter of the funciton /, actions.upN / success handler, optional /, console.error / error handler, optional / ) }), // Short hand of command only upLater2: n => ( state, actions/ actions of same level / ) => Cmd.ofPromise( upLater / a function with single parameter and return a promise /, n / the parameter of the funciton /, actions.upN / success handler, optional /, console.error / error handler, optional / ), }, view: () => {/...*/} , })
In Elm, we can intercept child component's message in parent component, because child's update function is called in parent's update function. But how can we do this in hydux?
import * as assert from 'assert' import * as Hydux from '../index' import Counter from './counter'const { Cmd } = Hydux
export function init() { return { state: { counter1: Counter.init(), counter2: Counter.init(), } } } const actions = { counter2: counter.actions, counter1: counter.actions } Hydux.overrideAction( actions, _ => _.counter1.upN, (n: number) => ( action, ps: State, // parent state (State) pa, // parent actions (Actions) // s: State['counter1'], // child state // a: Actions['counter1'], // child actions ) => { const { state, cmd } = action(n + 1) assert.equal(state.count, ps.counter1.count + n + 1, 'call child action work') return { state, cmd: Cmd.batch( cmd, Cmd.ofFn( () => pa.counter2.up() ) ) } } ) type State = ReturnType['state'] type Actions = typeof actions let ctx = Hydux.app({ init: () => initState, actions, view: noop, onRender: noop })
git clone https://github.com/hydux/hydux.git cd hydux yarn # or npm i cd examples/counter yarn # or npm i npm start
Now open http://localhost:8080 and hack!
After trying Fable + Elmish for several month, I need to write a small web App in my company, for many reasons I cannot choose some fancy stuff like Fable + Elmish, simply speaking, I need to use the mainstream JS stack but don't want to bear Redux's cumbersome, complex toolchain, etc anymore.
After some digging around, hyperapp looks really good to me, but I quickly find out it doesn't work with React, and many libraries don't work with the newest API. So I create this to support *different* vdom libraries, like React(official support), ultradom(built-in), Preact, inferno or what ever you want, just need to write a simple enhancer!
Also, to avoid breaking change, we have *built-in* support for HMR, logger, persist, Redux Devtools, you know you want it!
MIT