blup / deku

Functional view library for building UI components as an alternative to React

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool


got


version Circle CI js-standard-style

Gitter

A library for creating UI components using virtual DOM as an alternative to React. Deku has a smaller footprint (~10kb), a functional API, and doesn't support legacy browsers.

To install:

npm install deku

You can also use Duo, Bower or download the files manually.

Components are just plain objects that have a render function instead of using classes or constructors:

// button.js
let propTypes = {
  kind: {
    type: 'string',
    expects: ['submit', 'button']
  }
}

function render (component) {
  let {props, state} = component
  return <button class="Button" type={props.kind}>{props.children}</button>
}

function afterUpdate (component, prevProps, prevState, setState) {
  let {props, state} = component
  if (!state.clicked) {
    setState({ clicked: true })
  }
}

export default {propTypes, render, afterUpdate}

Components are then rendered by mounting it in a tree:

import Button from './button'
import {tree,render,renderString} from 'deku'

let app = tree(
  <Button kind="submit">Hello World!</Button>
)

render(app, document.body)

Trees can be rendered on the server too:

let str = renderString(app)

Docs

Components

Each element of your UI can be broken into encapsulated components. These components manage the state for the UI element and tell it how to render. In Deku components are just plain objects:

function render (component) {
  let {props, state} = component
  return <button class="Button">{props.children}</button>
}

export default {render}

There is no concept of classes or use of this. We can import this component using the standard module syntax:

import Button from './button'

Read more about components

Rendering Components

To render this to the DOM we need to create a tree. This is one of the other main differences between React and Deku. The tree will manage loading data, communicating between components and allows us to use plugins on the entire application.

import {element,tree} from 'deku'
var app = tree(<Button>Hello World</Button>)

The app object has only a couple of methods:

  • .set(name, value) to set environment data
  • .option(name, value) to set rendering options
  • .mount(vnode) to change the virtual element currently mounted
  • .use(fn) to use a plugin. The function is called with the app object.

You can render this tree anyway you like, you just need a renderer for it. Let's use the DOM renderer for the client:

import Button from './button'
import {element,tree,render} from 'deku'

var app = tree(<Button>Hello World</Button>)
render(app, document.body)

And render the same thing to a string on the server:

import koa from 'koa'
import {element,tree,renderString} from 'deku'

let app = koa()

app.use(function *() {
  this.body = renderString(tree(<Button>Hello World</Button>))
})

And you can isolate functionality by using plugins. These plugins can call set to add data to the tree that your components can then access through their props:

app.use(analytics)
app.use(router)
app.use(api(writeKey))

Composition

You can compose components easily by just requiring them and using them in the render function:

import Button from './button'
import Sheet from './sheet'

function render (component) {
  return (
    <div class="MyCoolApp">
      <Sheet>
        <Button style="danger">One</Button>
        <Button style="happy">Two</Button>
      </Sheet>
    </div>
  )
}

Event handlers

Deku doesn't use any form of synthetic events because we can just capture every event in newer browsers. There are special attributes you can add to virtual elements that act as hooks to add event listeners:

function render (component) {
  let {props, state} = component
  return <button onClick={clicked}>{props.children}</button>
}

function clicked () {
  alert('You clicked it')
}

You can view all event handlers in code.

Lifecycle hooks

Just like the render function, component lifecycle hooks are just plain functions:

function afterUpdate (component, prevProps, prevState, setState) {
  let {props, state} = component
  if (!state.clicked) {
    setState({ clicked: true })
  }
}

We have hooks for beforeMount, afterMount, beforeUpdate, afterUpdate, beforeUnmount and two new hooks - beforeRender and afterRender that are called on every pass, unlike the update hooks. We've found that these extra hooks have allowed us to write cleaner code and worry less about the state of the component.

Learn more about the lifecycle hooks

Validation

You can validate the props sent to your component by defining a propTypes object:

let propTypes = {
  style: {
    type: 'string',
    expects: ['submit', 'button']
  },
  danger: {
    type: 'boolean',
    optional: true
  }
}

To enable validation you just need to enable it on the tree:

app.option('validateProps', true)

This is off by default and we've made it an option so that you can enable it just during development without needing a separate build.

Props can originate from anywhere in the outside world, it's useful to validate them. When validation is enabled you'll only be able to pass in props that are defined and they must conform to the propTypes spec.

External data and communication

It's often useful for components to have access to data from the outside world without needing to pass it down through components. You can set data on your tree and components can ask for it using propTypes.

First we set some data on the app:

app.set('currentUser', {
  id: 12435,
  username: 'anthonyshort',
  name: 'Anthony Short'
})

Then in our components we define the prop using the source option:

let propTypes = {
  user: {
    source: 'currentUser'
  }
}

Whenever we change that value in our app all components that depend on it will be re-rendered with the latest value. We use this pattern to pass functions down to interact with the API:

app.set('updateProject', function (project, updates) {
  api.projects.update(project, updates)
})

Which the component can access using props.updateProject. Although it may not be as complex or optimized as Relay and GraphQL it's extremely simple and covers most use cases we've run into so far. We even use this pattern to treat the router as a data source:

router.on('/projects/:id', function (params) {
  let project = api.projects.get(params.id)
  app.set('currentRoute', {
    name: 'view project',
    project: project
  })
})

This means we don't need to use some complex routing library. We just treat it like all other types of external data and components will render as needed.

Keys

Sometimes when you're rendering a list of items you want them to be moved instead of trashed during the diff. Deku supports this using the key attribute on components:

function render (component) {
  let {items} = component.props
  let projects = items.map(function (project) {
    return <ProjectItem key={project.id} project={project} />
  })
  return <div class="ProjectsList">{projects}</div>
}

At the moment we only support the key attribute on components for simplicity. Things become slightly more hairy when moving elements around within components. So far we haven't ran into a case where this has been a major problem.

Tests

Sauce Test Status

Developing

Deku is built with Browserify. You can run the tests in a browser by running make test.

License

MIT. See LICENSE.md

About

Functional view library for building UI components as an alternative to React

License:MIT License


Languages

Language:JavaScript 99.3%Language:Makefile 0.6%Language:HTML 0.1%