Zedux
The complete state management solution. Zedux is a futuristic Redux.
Feature list of awesomeness
- composable stores – Yep, for real. Enter the Higher-Order Store revolution!
- built-in side effects model – Thunks, generators, and observables out-of-the-box.
- reducer-driven state updates – Redux-style power. Optional, of course.
- state machines
- code splitting
- zero-configuration – Opinionated but configurable.
- memoized selectors
Installation
Install using npm:
npm install --save zedux
Or yarn:
yarn add zedux
Or include the appropriate unpkg build on your page (module exposed as Zedux
):
Development
<script src="https://unpkg.com/zedux/dist/zedux.js"></script>
Production
<script src="https://unpkg.com/zedux/dist/zedux.min.js"></script>
Getting started
To learn by example, check out the examples doc page or the examples in the repo.
To learn by getting dirty, have a play with this codepen.
To learn from us, check out the documentation.
To learn comprehensively, check out the tests.
Or keep reading for a brief run-down:
Intro
- composition > middleware
This is the core philosophy of Zedux. Zedux takes Redux and dips it in React's composable architecture to achieve stateful zen.
- Opinionated but configurable.
Simplicity rules. Zedux stores require zero configuration to start. But they're flexible and powerful enough to move with you as your app's state demands increase. Apps of all sizes, from tiny to gigantic, should be able to use Zedux comfortably.
- Building blocks of state
The store is the basic building block of application state. Stateful components that expose a Zedux store can be easily consumed and composed in any application. Zedux stores can also be easily synced across realms (workers, browser extensions, iframes, SSR).
Composition is king.
Quick start
At the most basic level, Zedux is still Redux. A reducer hierarchy drives state creation and updates. Let's get some code:
import { act, createStore, react } from 'zedux'
/*
Meet your first Zedux store.
He's a fast, composable, predictable state container.
And the best part is that he's all ready to go.
*/
const store = createStore()
/*
These are actors.
An actor is just a function that returns an action.
An action is just a plain object with a "type" property.
Actions are how we tell the store to update.
(For Redux peoples): An actor is just a fancy action creator.
*/
const increment = act('increment')
const decrement = act('decrement')
/*
This is a reactor (in Zedux we emphasize the act-react
relationship between actions and reducers).
A reactor is just a fancy reducer.
A reducer is just a pure function with the signature:
(state, action) => state
This reactor translates the "increment" and "decrement"
actions into a corresponding state update by delegating
to sub-reducers.
A sub-reducer is just a reducer that doesn't control its
initial state and is only called for specific actions.
*/
const counterReactor = react(0) // 0 - the initial state
.to(increment)
.withReducers(state => state + 1)
.to(decrement)
.withReducers(state => state - 1)
/*
So we said the store is all ready to go. And that's true.
But in Zedux, zero configuration is optional <fireworks here>.
We're not gonna use it here. But check it out in the docs.
Here we're introducing our reactor hierarchy to Zedux.
In this example it's a very simple one, but `store.use()`
actually accepts complex objects containing reactors, stores,
and nested objects containing reactors, stores, and...you
get the picture.
*/
store.use(counterReactor)
/*
Here we're subscribing to the store.
Zedux calls this function every time the store's state changes.
*/
store.subscribe((oldState, newState) => {
console.log(`counter went from ${oldState} to ${newState}`)
})
/*
Alright! Let's take this thing for a spin!
We will do said spin by dispatching actions to the store.
*/
store.dispatch(increment())
// counter went from 0 to 1
store.dispatch(increment())
// counter went from 1 to 2
store.dispatch(decrement())
// counter went from 2 to 1
If you know Redux, almost all of this will seem familiar (and not just because the Redux docs have a similar example). At this point, you should know enough to get started using Zedux. But don't worry, there's a ton of cool stuff we haven't covered.
Here's a small taste of what's in store (yes, that pun was an accident):
Zero configuration
Zedux stores are so dynamic, you can just create one and go. Here's what the above counter example looks like if we leverage zero configuration:
import { createStore } from 'zedux'
/*
We use `store.hydrate()` to force-update the store's
entire state tree:
*/
const store = createStore()
.hydrate(0) // set the initial state to 0
/*
These are inducers.
Inducers are like reducers (hence the name), but have the form:
state => partialStateUpdate
Inducers are dispatchable - they can be passed directly to
`store.dispatch()`
*/
const increment = state => state + 1
const decrement = state => state - 1
/*
And that's it! The fun starts now.
*/
store.dispatch(increment)
store.dispatch(decrement)
/*
Inducers are super easy to create on the fly...
*/
store.dispatch(state => state + 6)
/*
...but in this case we'll typically want to use `store.setState()`
*/
store.setState(9)
Dispatching an inducer and calling store.setState()
are functionally similar, but have different use cases. Inducers are nice for creating predefined state updater packages. store.setState()
is nice for on-the-fly state updates – use cases are similar to React's setState()
.
The advantage of using inducers and store.setState()
over store.hydrate()
is that the new state is deeply merged into the existing state. Thus headaches like:
store.hydrate({
...state,
todos: {
...state.todos,
urgent
}
})
become simple:
store.setState({
todos: { urgent }
})
// or, with an inducer:
store.dispatch(() => ({
todos: { urgent }
}))
since Zedux clones the nested nodes for us.
You may have noticed that the branch nodes of our state trees are all plain objects. But Zedux can actually be taught to understand any hierarchical data type. Immutable fans rejoice and check out the guide on configuring the hierarchy.
But what about time travel??
Ooh. You're gonna love this. Zedux translates every pseudo-action into a serializable action that a store's inspectors can plug in to. store.hydrate()
, store.setState()
, and actions/inducers dispatched to child stores will all find a way to notify a store's inspectors of a serializable action that can be used to reproduce the state update. In short, you never have to worry about whether a state update is reproducible. Zedux has you covered.
See:
Store composition
Too good to be true? Think again. The store composition model of Zedux is unprecedented and extremely powerful. The Zedux store's disposable and highly performant nature combined with its uncanny time traveling ability will make you weep. With joy, of course.
import { act, createStore, react } from 'zedux'
/*
While a Zedux app can have many stores, we'll typically always
create a single "root" store. This gives us the best of both
worlds - The time traveling ability of the singleton model
and the encapsulation of component-bound stores.
*/
const rootStore = createStore()
// A basic inducer
const increment = state => state + 1
let storeIdCounter = 1
/*
A simple factory for creating a "counter" store and attaching
it to the root store's reactor hierarchy.
*/
const createCounterStore = () => {
const storeId = storeIdCounter++
const counterStore = createStore()
.hydrate(0)
// Where the magic happens; tell rootStore to "use" counterStore
rootStore.use({ [`counter${storeId}`]: counterStore })
return counterStore
}
// And enjoy
const counter1 = createCounterStore()
const counter2 = createCounterStore()
// We can increment each counter individually...
counter1.dispatch(increment)
counter1.dispatch(increment)
counter2.dispatch(increment)
// ...or the whole lot of 'em:
rootStore.dispatch(increment)
rootStore.getState() // { counter1: 3, counter2: 2 }
Treating the store as the basic application building block opens the door for embedded applications. The Zedux store is an autonomous unit that can simultaneously handle a sub-module's internal workings and present a standardized api to consumers.
Additionally, the ability to create stores whose lifecycle parallels the lifecycle of a component while still maintaining time-traversable state and replayable actions is an exciting new possibility that Zedux has blown wide open.
See:
Selectors
Zedux ships with a basic api for creating one of the most powerful state management performance tools: Memoized selectors. A selector is just a function with the form:
state => derivedState
In other words, it takes a state tree and plucks a piece off of it and/or applies some transformation to it. A memoized selector is a smart selector that only recalculates its value when absolutely necessary. When a recalculation is not necessary, it returns a cached value.
import { select } from 'zedux'
/*
This is a normal selector.
He just grabs the list of todos off the state tree.
*/
const selectTodos = state => state.entities.todos
/*
This is a memoized selector.
He consists of a list of input selectors and a calculator
function. The calculator function's arguments are the outputs
of the input selectors. The calculator function will only be
called when BOTH the state and the output of one or more
input selectors change.
*/
const selectIncompleteTodos = select(
selectTodos,
todos => todos.filter(todo => !todo.isComplete)
)
Selectors are an absolutely necessary ingredient for well-managed state. Use them. Use them all the time. Memoized selectors carry some overhead, so only use them when the performance benefit is obvious – e.g. when iterating over a list.
See:
State machines
Don't get too excited. But yes, state machines are very powerful and yes, Zedux includes a basic implementation.
A state machine is just a graph. The possible states are the nodes of the graph. The possible transitions between states are directed edges connecting the nodes.
import { state, transition } from 'zedux'
/*
Behold the states.
A state is just a fancy actor.
*/
const open = state('open')
const closing = state('closing')
const closed = state('closed')
const opening = state('opening')
/*
Once we have our states, we create the machine by defining
how the machine transitions from one state to the next.
A machine is just a fancy reactor.
*/
const doorMachine = transition(open)
.to(closing)
.to(opening, closed)
.from(opening)
.to(closing, open)
.from(closed)
.to(opening)
/*
Since our doorMachine is just a very fancy reducer,
it's super easy to test (and just have a blast with).
*/
doorMachine(open.type, closing()) // closing - valid transition
doorMachine(closing.type, open()) // closing - invalid transition
doorMachine(opening.type, open()) // open - valid transition
See:
To be continued...
At this point you should have a pretty good idea of what Zedux is all about. Check out the full documentation for the rest of the awesomeness.
Official packages
- React Zedux - Official React bindings for Zedux.
- Zedux Immer - Official Immer bindings for Zedux.
It seems too big
Mm. It really isn't. You may think this because Redux is so tiny. But compare it to something like Rx, which is a complete solution in its field, and Zedux is very tiny. Currently it's almost twice the size of Redux, but accomplishes way more than twice as much. In fact, given the reduced number of plugins, your app's dependencies will almost certainly be smaller with Zedux than with Redux.
That said, if we find that anything is not used enough to make it worth including in Zedux core, we'll definitely take it out. On the flip side, if any features are sorely needed and not included, we'll gladly consider including them. Zedux has only a faint coat of feature-creep repellent.
Contributing
All contributions on any level are so overwhelmingly welcome. Just jump right in. Open an issue. PRs, just keep the coding style consistent and the tests at 100% (branches, functions, lines, everything 100%, plz). Let's make this awesome!
Bugs can be submitted to https://github.com/bowheart/zedux/issues
License
The MIT License.