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 hydux@^v0.5.8
import { inject } from 'hydux'
export default {
init: () => ({ count: 1 }),
actions: {
down() {
let { state, actions, setState, Cmd } = inject<State, Actions>()
setState({ count: state.count - 1 })
Cmd.addSub(_ => _.log('down -1'))
actions.up()
},
up() {
let { state, actions } = inject<State, Actions>()
return { count: state.count + 1 }
},
log(msg) {
console.log(msg)
}
},
view: (state, actions) => {
return (
<div>
<h1>{state.count}</h1>
<button onclick={actions.down}>–</button>
<button onclick={actions.up}>+</button>
</div>
)
}
}
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) =>
<div>
<h1>{state.count}</h1>
<button onclick={actions.down}>–</button>
<button onclick={actions.up}>+</button>
</div>
}
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,
) =>
<main>
<h1>Counter1:</h1>
{Counter.view(state.counter1, actions.counter1)}
<h1>Counter2:</h1>
{Counter.view(state.counter2, actions.counter2)}
</main>
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 object `actions` 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.
// 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) => (
<main>
<h1>Counter1:</h1>
{Counter.view(state.counter1, actions.counter1)}
<h1>Counter2:</h1>
{Counter.view(state.counter2, actions.counter2)}
</main>
)
export type Actions = typeof actions
export type State = ReturnType<typeof init>['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) => (
<main>
<h1>Counter1:</h1>
{subComps.render('counter1', state, actions)}
// euqal to:
// {subComps.views.counter1(state.counter1, actions.counter1)}
// .render('<key>', ...) 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.
<h1>Counter2:</h1>
{subComps.render('counter2', state, actions)}
</main>
)
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<typeof init>['state']
type Actions = typeof actions
let ctx = Hydux.app<State, Actions>({
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