Stateless virtual dom <-> Redux.
$ npm install vdux
$ cd examples/basic && budo --live index.js -- -t babelify
import {dom as vdux, element} from 'vdux'
import ready from 'domready'
/**
* Initialize the app
*/
const initialState = {
counter: 0
}
function reducer (state, action) {
if (action.type === 'INCREMENT') {
return {...state, counter: state.counter + 1}
}
return state
}
ready(() => vdux({
reducer,
initialState,
app: state => <Counter value={state.counter} />
}))
/**
* App
*/
const Counter = {
render ({props}) {
return <div onClick={increment}>Value: {props.value}</div>
}
}
function increment () {
return {
type: 'INCREMENT'
}
}
vdux is an opionated, higher-level abstraction over redux and virtex. To initialize it, it takes a single object containing several parameters:
middleware
- An array containing redux middleware you want to use. Defaults to[]
.reducer
- Your root redux reducerinitialState
- The initial state atom for your application. Defaults to{}
.app
- A function that acceptsstate
and returns a virtual node treenode
- The root node you want to renderapp
into. Defaults todocument.body
.
Once you call vdux with these parameters, you're done. Your application's primary state <-> UI loop is established. From this point forward, nothing your code does will be imperative - every function will accept parameters and return a value, and that's it.
vdux's JSX pragma is accessible on vdux/element
. E.g.
import element from 'vdux/element'
export default function render () {
return <div>Hello world!</div>
}
Put this in your babelrc and npm install babel-plugin-transform-react-jsx
to make JSX work with element
.
"plugins": [
["transform-react-jsx", {"pragma": "element"}]
]
Your event handlers are pure functions that return a value. That value is then dispatched into redux. This forms a cycle that will define your entire application in a side-effect free way.
Using hyperscript:
import h from 'vdux/element'
function counter ({props}) {
return h('div', {'onClick': increment}, ['Value: ' + props.counter])
}
function increment () {
return {
type: 'INCREMENT'
}
}
Or using JSX:
import element from 'vdux/element'
function render ({props}) {
return <div onClick={increment}>Value: {props.counter}</div>
}
function increment () {
return {
type: 'INCREMENT'
}
}
export default render
Components in vdux look a lot like components in other virtual dom libraries. You have a render
, and some lifecycle hooks. Your render
function receives a model
that looks like this:
props
- The arguments passed in by your parentchildren
- The child elements of your componentstate
- The state of your componentlocal
- Directs an action to the current component's reducer (see the local state section)ref
- Direct an action to another component's reducer (see the local state section)path
- The dotted path to your component in the DOM tree. For the most part, you probably don't need to worry about this.
By default, this is taken care of for you. virtex-component assumes that your data is immutable, and will do a shallow equality check on props
and children
to decide if your component needs to re-render. If you want to implement your own, it works like this (this is the one virtex-component uses):
function shouldUpdate (prev, next) {
return !arrayEqual(prev.children, next.children) || !objectEqual(prev.props, next.props)
}
Where prev
is the previous model and next
is the next model.
beforeMount(model)
- Called before the component is rendered in the DOM for the first time. Called only once.beforeUpdate(prev, next)
- Called before a change to the component's model (not including the initial render).beforeRender(model)
- Called right before any render (including the initial render).afterUpdate(prev, next)
- Called after a change to the component's model (not including the initial render).afterMount(model)
- Called after the component is first rendered into the DOM. Called only once.
In vdux, all of your state is kept in your global redux state atom under the ui
property. In order to support component local state, your component may export two additional functions:
initialState
- Receivesmodel
and returns the starting state for your component.reducer
- A reducing function for your component's state. Receives(state, action)
and returns a new state.
The local
function is how you update the state of your component. It accepts a function that returns an action, and returns a function that creates an action directed to the current component. That's may be a bit hard to digest, so here's an example:
function initialState () {
return {
value: 0
}
}
function render ({local, state}) {
return <div onClick={local(increment)}>Counter: {state.counter}</div>
)
}
function counter () {
return {
type: 'INCREMENT'
}
}
function reducer (state, action) {
if (action.type === 'INCREMENT') {
return {
value: state.value + 1
}
}
}
export default {
initialState
render,
reducer
}
Ref is how you talk to other components in a functional way. It has two functions on it
as(name)
- Creates an aliased reference to a child component.to(name, fn)
- Directs an action (returned byfn
) to the component aliased asname
.
Here's an example:
import Dropdown from 'components/dropdown'
function render ({ref}) {
return (
<button onClick={ref.to('dropdown', Dropdown.open)}>Open Dropdown</button>
<Dropdown ref={ref.as('dropdown')}>
<li>item</li>
</Dropdown>
)
}
In dropdown.js you'd just export a plain open
action creator function, like this:
function open () {
return {
type: 'OPEN'
}
}
function reducer (state, action) {
if (action.type === 'OPEN') {
return {
...state,
open: true
}
}
return state
}
export default {
open,
reducer
}
Internally, all ref.to
is doing is using <Dropdown/>
's local
function to send it a message in the same way it sends actions to itself.
...Still working on this section...
Almost side-effect free, anyway. You still need to do things like issue requests. I recommend you contain these effects in your redux middleware stack. Here are some libraries / strategies for doing this:
- redux-multi - Highly recommended, without this, you'll only be able to return one action at a time, which is severely limiting. Use this to return arrays of actions to be dispatched.
vdux itself is very small. It is primarily composed of other, smaller modules:
- redux - Functional state container.
- virtex - The redux-based virtual dom library used by vdux. You don't need to use this - it's already in vdux, but you will need to add virtex middleware to your redux middleware stack.
- virtex-element - A high-level, opionated JSX pragma for virtex nodes. You probably want to use this for getting started, but later on you may be interested in adding your own sugary element creators.
- virtex-dom - DOM rendering redux middleware backend for virtex. You need this if you want to be rendering DOM nodes.
- virtex-string - String rendering redux middleware backend. Use this for server-side rendering and tests.
- virtex-component - Lets virtex understand components. Adds nice react/deku-style components with hooks,
shouldUpdate
, and other civilized things. - virtex-local - Give your components local state, housed inside your redux state atom. Note that you will also need redux-ephemeral mounted into your reducer for this to work.
- redux-ephemeral - Allows your reducer to manage transient local state (i.e. component local state).
If you want to try something more advanced, you can create your own vdux by composing these modules and inserting others in your own way.
The MIT License
Copyright © 2015, Weo.io <[email protected]>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.