vdux
v3.0.10
Published
Stateless virtual dom <-> Redux
Downloads
148
Readme
vdux
Stateless virtual dom <-> Redux.
Installation
$ npm install vdux
Running the examples
$ cd examples/basic && budo --live index.js -- -t babelify
Philosophy
vdux is an opinionated bridge between a virtual DOM library and redux. It takes the react/redux philosophy and flips it on its head. Vdux believes that all state should be component-local and that the component is the sole, fundamental building block of your application. Components may bring their own middleware and ought to control the application lifecycle in its entirety.
Why all local state? Because almost all state has some sort of lifecycle. If you have global state, put it in your top-most component. The thing that I think mostly drove the concept of global state was server-side rendering. But that is solved by vdux's other innovation: all your local state is stored in a single, accessible, immutable state atom. You can reconstitute your entire state tree from the server on the client, with no exceptions. You can do that very simply, like this:
function * serverRender () {
const {state, html} = yield vdux(<App />)
this.body = `<html>
<head>
<script src='/vdux.js'></script>
<script src='/app.js'></script>
<script>
vdux(() => <App />, {
initialState: ${JSON.stringify(state),
prerendered: true
})
</script>
</head>
<body>
${html}
</body>
</html>`
}
Minimal counter example
import vdux from 'vdux/dom'
import {component, element} from 'vdux'
/**
* Initialize the app
*/
vdux(() => <App />)
/**
* App
*/
const App = component({
render ({state, actions}) {
return <div onClick={actions.increment}>Value: {state.value}</div>
},
reducer: {
increment: state => ({
value: state.value + 1
})
}
}
Usage
vdux is an opionated, higher-level abstraction over redux and virtex. You initialize it in the browser like this:
import vdux from 'vdux/dom'
import App from 'components/App'
vdux(() => <App />)
This returns an object with a few functions:
dispatch(action)
- Manually dispatch an action. If you have outside event sources or want to dispatch manually for testing purposes, use this.getState()
- Returns the current redux state atom.stop()
- Stop the application cycle.forceRerender()
- Forces a full rerender. Useful for hot reloading.
JSX / Hyperscript
import {element} from 'vdux'
export default function render () {
return <div>Hello world!</div>
}
babelrc
Put this in your .babelrc
and npm install babel-plugin-transform-react-jsx
to make JSX work with vdux's element
creator.
"plugins": [
["transform-react-jsx", {"pragma": "element"}]
]
DOM Events / Actions
Your event handlers are generator functions or state reducers specified by your component. They will be specified in the controller
and reducer
properties, respectively.
import {component, element} from 'vdux'
import sleep from '@f/sleep'
export default component({
render ({state, actions}) {
return (
<div onClick={actions.increment}>Value: {state.counter}</div>
<button onClick={actions.incrementAsync(100)}>Increment Async</button>
)
},
controller: {
* incrementAsync ({actions}, milliseconds) {
yield sleep(milliseconds)
yield actions.increment()
}
},
reducer: {
increment: state => ({
state: state.value + 1
})
}
})
Autocurrying
All your actions will autocurry indefinitely. That means that you can pass down a curried action like this: updateEntity(id)
, and then the component below you can add more parameters to it. Internally, the arguments you are accumulating will be exposed on the function that's passed down. This means that vdux can diff your actions in a meaningful way, so passing functions down through many layers of your application will not create performance problems.
Event names
Also of note, vdux is unopinionated about the casing of event handler prop names. If you are wondering if it's onKeyDown
or onKeydown
, both/all will work, even ONKEYDOWN
, or onkeydown
. As long as it matches the case-insensitive regex on(?:domEventNames.join('|'))
, it'll work. If you want to know whether an event you want is included, check out dom-events for the complete list - and if one you want isn't there, just send a PR to that module.
Events
If you want to do more than one thing in response to an event, you can pass an array of handlers, like this:
function render () {
return <div onClick={[actions.fetchPosts, actions.closeDropdown]}></div>
}
The return values of both handlers will be dispatched into redux. There is also a set of special syntax for keyboard related events - you may pass an object containing the particular keychords you want to select for. E.g.
function render () {
return <input onKeydown={{enter: actions.submit, esc: actions.cancel, 'shift+enter': actions.newline}} />
}
Decoders
By default, most of your event handlers will receive no arguments (unless you curry them) with the exception of the input
and change
events, which by default receive event.target.value
. However, if you need to access something else, or if you want to stopPropagation/preventDefault
, you can use a 'decoder'. This is a concept borrowed from Elm. Decoders get access to the raw event and can transform it and return something suitable for consumption by your handler. vdux comes with a number of default decoders:
- decodeRaw - Passes
event
- decodeNode - Passes
e.target
- decodeValue - Passes
e.target.value
- decodeFiles - Passes
e.target.files
- decodeMouse - Passes
{clientX: e.clientX, clientY: e.clientY}
You can import any of these from vdux
and use them like this:
component({
render ({actions}) {
return (
<div onMouseMove={decodeMouse(actions.updateCoords)} style={{width: '400px', height: '400px'>
x: {state.clientX}
y: {state.clientY}
</div>
)
},
reducer: {
updateCoords: (state, coords) => coords
}
})
Custom decoders
If you need to implement your own custom decoder, you can import decoder
from vdux, like this:
import {decoder} from 'vdux'
const decodeRelatedTarget = decoder(e => e.relatedTarget)
stopPropagation / preventDefault
You can do this one of two ways. You can use decodeRaw
and call these methods yourself on the raw event, or you can use the special declarative handlers vdux provides you. Example:
import {component, element, stopPropagation} from 'vdux'
component({
render () {
return <button onClick={[actions.handleClick, stopPropagation]} />
}
})
Inline styles
element
includes some minimal inline style sugar for you. It won't do autoprefixing or anything complicated, but you can pass in a basic style object and have it turned into a style string, automatically. E.g.
function render () {
return <div style={{color: 'red', fontWeight: 'normal'}}></div>
}
Will produce a style string of 'color: red; font-weight: normal'
.
Class names
The classnames module is used for this. So you can do:
function render () {
return <div class={['primary', 'button']}>hello world</div>
}
Or you can do:
function render ({props}) {
return <div class={{primary: !!props.primary, button: true}}>hello world</div>
}
You can also recursively mix and match these things, like ['class1', {class2: props.class2}]
.
Components
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 componentcontext
- A context object specified by your root componentactions
- Directs an action to the current 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.
Hooks
onCreate
- When the component is created. Receivesmodel
.onUpdate
- When the model changes. Receivesprev
andnext
models.afterRender
- Called after any render. Passed the model and the root DOM node of the component. Runs only in the browser. It is recommended that you avoid using it as much as possible - but it is necessary in a few cases like positioning elements relative to one another.onRemove
- When the component is removed. Receivesmodel
.
afterRender / nextTick
The afterRender function can be used to do things after the element has been parented in the DOM. But sometimes it is also important to do something in your afterRender, and then on precisely the next tick before the next render, to do something else (e.g. adding an 'active' class that initiates an animation). You can do this in a guaranteed, safe way by returning a function or array of functions from your afterRender
. This function is guaranteed to be executed on the next tick and before any additional render calls. E.g.
function afterRender ({props}, node) {
if (props.entering) {
addClass(node, 'enter')
return () => addClass(node, 'enter-active')
}
}
Context
Sometimes it's too cumbersome to pass everything down from the top of your app. Things like the current url, logged in user, or theme might be too ubiquitous at the leaves of your tree to warrant manually propagating them down via props. For these cases, there is a way out: Context. Context let's you define an object at the top level that any component in the tree can tap into. It is specified by the top-most component of your app. Only your root component may specify a getContext
function. There is no layering of context.
/**
* <App/>
*/
export default component({
getContext ({actions, state}) {
return {
url: state.url,
currentUser: state.currentUser,
logUserIn: actions.logUserIn
logUserOut: actions.logUserOut
}
},
render ({state}) {
return <Router url={state.url} />
}
// ...
})
From then on, context
will be available in every component's model. Any time context changes, the entire application will be rerendered, so do not put things in there that change often. Note: it is particularly useful to put actions that manipulate your top-level state into your context. This allows you to pass global actions down to your lower-level components in a clean way.
Global event handlers
Sometimes you want to listen to events on the window or the document in your components. For instance, to close a dropdown on an external click. This can be awkward and error prone, because you have to store a reference to the handler, and then keep it in sync with the life-cycle of your component. To address this vdux exports some special components to make this nice for you, Window
, Document
and Body
, that allow you to bind event handlers to each of these elements in the exact same way you bind to any other events.
Example - Closing a dropdown on an external click
import Document from 'vdux/document'
function render ({local, children}) {
return (
<Document onClick={local(close)}>
<div class='dropdown'>
{children}
</div>
</Document>
)
}
Example - Router
import Window from 'vdux/window'
import Document from 'vdux/document'
import HomePage from 'pages/home'
import enroute from 'enroute'
const router = enroute({
'/': () => <HomePage />
})
function render ({local, state}) {
return (
<Window onPopstate={local(setUrl)}>
<Document onClick={handleLinkClicks(local(setUrl))}>
{
router(state.url)
}
</Document
</Window>
)
}
function handleLinkClicks (setUrl) {
return e => {
if (e.target.nodeName === 'A') {
const href = e.getAttribute('href')
if (isLocal(href)) {
e.preventDefault()
return setUrl(href)
}
}
}
}
Hot module replacement
Since vdux itself is largely stateless, hot module replacement is trivial. All the code you need is:
const {forceRerender} = vdux(() => <App />)
if (module.hot) {
module.hot.accept(['./app'], () => {
App = require('./app')
forceRerender()
})
}
Server-side rendering
Server-side rendering uses vdux/string
. And its interface is essentially the same as the DOM renderer:
import vdux from 'vdux/string'
function * render (req) {
const {html, state} = yield vdux(() => <App req={req} />)
this.body = page(html, state)
}
Delayed server-side rendering
If you just use the code above, your app will simply return its initial render. However, often you may want to grab the current user and other information from your API/DB server, and this may take an indeterminate amount of time before you decide that the app is 'ready' to be rendered. If you want to do this, you need to pass awaitReady
into vdux's second parameter, like this:
import vdux from 'vdux/string'
function * render (req) {
const {html, state} = yield vdux(() => <App req={req} />, {awaitReady: true})
this.body = page(html, state)
}
This will cause the yield
to block until the appReady
action is dispatched. You can do that like this:
import fetchMw, {fetch} from 'redux-effects-fetch'
import {appReady, component, element} from 'vdux'
import InternalApp from 'components/InternalApp'
import Loading from 'components/Loading'
import cookie from 'cookie'
/**
* <App/>
*/
export default component({
initialState ({props}) {
if (props.req) {
const cookieObj = cookieParser.parse(props.req.headers.cookie || '')
return {
currentUrl: props.req.url,
authToken: cookieObj.authToken || ''
}
}
},
onCreate ({actions}) {
return actions.fetchUser()
}
render ({state}) {
return state.user ? <InternalApp/> : <Loading/>
},
onUpdate (prev, next) {
if (!prev.state.user && next.state.user) {
return appReady()
}
},
middleware: [
fetch
],
controller: {
* fetchUser ({state, props}) {
if (!state.user && !state.userLoading && props.req) {
const userId = cookie(props.req.headers['Cookie'])
yield actions.userLoading()
const user = yield fetch(`/user/${userId}`, {
headers: {
Authentication: 'Bearer ' + state.authToken
}
})
yield actions.userLoaded(user)
}
}
},
reducer: {
userLoading: () => ({userLoading: true}),
userLoaded: (state, user) => ({
userLoading: false,
user
})
}
})
Middleware
Your components may also specify custom middleware. This can enable them to run side-effects in a functionally pure way. Your component middleware will receive all the same things that redux middleware does a getContext
function to retrieve the local context. Additionally the getState
function that the middleware receives will be the state of the local component that it was installed in.
Your middleware will receive any actions yielded by your controller methods, and they will receive them first. This means that you can use your middleware to debounce your actions or otherwise transform them, or simply enable new side-effectful actions. An example of debouncing/throttling middleware is redux-timing.
Different middleware for different environments
You can specify your middleware as an array, but you can also specify an object with different environment keys, like this:
middleware: {
node: [
// Node-specific middleware
],
browser: [
// Browser-specific middleware
],
shared: [
// Shared middleware
]
}
Components
Take a look at the org vdux-components for more.
- ui - Large stateless kit of UI components
- containers - Stateful wrappers around all of the vdux-ui components that add some features like
hoverProps
, etc.. - delay - Delay the rendering of child components, or execution of an action for a declaratively specified period.
- hover - Hover component
License
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.