A light-weight Elm-like alternative for Redux ecosystem, inspired by Hyperapp and Elmish.
- Elm Architecture, split your whole app with init, state, actions.
- hyperapp compatible API
- Elm-like side effect manager and subscribe API
- Support any vdom library, including react (official support)
- Router (Recommended)
- Official support for react-router
- Hot reload (hmr)
- Server-Side Rendering(SSR)
- code splitting, seamlessly integrated with SSR.
- logger, persist, Redux Devtools with time traveling, ultradom(1kb vdom), **All in One**, easily setup all these fancy stuff without pain!
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
})
- hydux-react: Hydux's react integration
- hydux-preact: Hydux's preact integration
- hydux-react-router: Hydux's react-router integration
- hydux-mutator: A statically-typed immutable update help package, which also contains immutable collections.
- hydux-transitions: A css transition library inspired by animajs's timeline, follow The Elm Architecture.
- hydux-data: Statically-typed data-driven development for hydux, in the Elm way. Inspired by apollo-client.
- hydux-pixi: High performance pixi.js renderer for Hydux.
- hydux code snippets for vscode (TS) Best practice for using hydux + typescript without boilerplate code.
- samples-antd: Admin sample in hydux.
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