Skip to content

Latest commit

 

History

History
365 lines (270 loc) · 9.36 KB

README.md

File metadata and controls

365 lines (270 loc) · 9.36 KB

React Derivable

Travis build status npm

React Derivable allows to define React components which re-render when reactive values (defined in terms of derivable) used in render() change.

Table of Contents

Installation

Install from npm (react and derivable are peer dependencies and must be installed for an application too):

% npm install react
% npm install derivable@1.0.0-beta10
% npm install react-derivable

Usage

Define your application state in terms of derivable:

import {atom} from 'derivable'

let message = atom('Hello, World!')

Define a React component which accepts and uses in render a reactive value message:

import React from 'react'

let Hello = props =>
  <div>{props.message.get()}</div>

Now produce a new reactive component using higher-order reactive component

import reactive from 'react-derivable'

let ReactiveHello = reactive(Hello)

Render <ReactiveHello /> into DOM and pass it a reactive message value:

import ReactDOM from 'react-dom'

ReactDOM.render(<ReactiveHello message={message} />, ...)

Each time reactive value updates - component gets rerendered:

message.set('Works!')

API

reactive(Component)

As shown in the usage section above reactive(Component) decorator produces a reactive component out of an original one.

Reactive components re-render when one of the reactive values referenced from within render() change.

import React from 'react'
import {reactive} from 'react-derivable'

let ReactiveFunctional = reactive(props =>
  <div>{props.message.get()}</div>)

let ReactiveClassBased = reactive(class extends React.Component {

  render() {
    return <div>{this.props.message.get()}</div>
  }
})

pure(Component)

Makes component reactive and defines shouldComponentUpdate which compares props and state with respect to reactive values.

That allows to get rid of unnecessary re-renders.

import React from 'react'
import {pure} from 'react-derivable'

let PureFunctional = pure(props =>
  <div>{props.message.get()}</div>)

let PureClassBased = pure(class extends React.Component {

  render() {
    return <div>{this.props.message.get()}</div>
  }
})

pure(Component).withEquality(eq)

Same as using pure(Component) but with a custom equality function which is used to compare props/state and reactive values.

Useful when using with libraries like Immutable.js which provide its equality definition:

import * as Immutable from 'immutable'
import {pure} from 'react-derivable'

let Pure = pure(Component).withEquality(Immutable.is)

Guides

Local component state

React has its own facilities for managing local component state. In my mind it is much more convenient to have the same mechanism serve both local component state and global app state management needs. That way composing code which uses different state values and updates becomes much easier. Also refactorings which change from where state is originated from are frictionless with this approach.

As any component produced with reactive(Component) reacts on changes to reactive values dereferenced in its render() method we can take advantage of this.

Just store some atom on a component instance and use it to render UI and update its value when needed.

That's all it takes to introduce local component state:

import {Component} from 'react'
import {atom} from 'derivable'
import {reactive} from 'react-derivable'

class Counter extends Component {

  counter = atom(1)

  onClick = () =>
    this.counter.swap(value => value + 1)

  render() {
    return (
      <div>
        <div>{this.counter.get()}</div>
        <button onClick={this.onClick}>Next</button>
      </div>
    )
  }
}

Counter = reactive(Counter)

Flux/Redux-like unidirectional data flow

Flux (or more Redux) like architecture can be implemented easily with reactive values.

You would need to create a Flux architecture blueprint as a function which initialises an atom with some initial state and sets up action dispatching as a reducer (a-la Redux):

import {atom} from 'derivable'

function createApp(transformWithAction, initialState = {}) {
  let state = atom(initialState)
  return {
    state: state.derive(state => state),
    dispatch(action) {
      let transform = transformWithAction[action.type]
      state.swap(state => transform(state, action))
    }
  }
}

Now we can use createApp() function to define an application in terms of initial state and actions which transform application state:

const CREATE_TODO = 'create-todo'

let todoApp = createApp(
  {
    [CREATE_TODO](state, action) {
      let todoList = state.todoList.concat({text: action.text})
      return {...state, todoList}
    }
  },
  {todoList: []}
)

function createTodo(text) {
  todoApp.dispatch({type: CREATE_TODO, text})
}

Now it is easy to render app state into UI and subscribe to state changes through the reactive(Component) decorator:

import React from 'react'
import {reactive} from 'react-derivable'

let App = reactive(() =>
  <ul>
    {todoApp.state.get().todoList.map(item => <li>{item.text}</li>)}
  </ul>
)

Binding to external state sources

Sometimes state is originated not from application but from some external sources. One notorious example is routing where state is stored and partially controlled by a browser.

It is still useful to have access to that state and do it using the homogenous API.

Like we already discovered we can use derivable library to implement local component state and flux like state management easily. Let's see how we can use derivable to implement routing based on browser navigation state (HTML5 pushState API).

We'll be using the history npm package which makes working with HTML5 API smooth and simple.

First step is to make a history object which will hold the navigation state and some methods to influence those state:

import {createHistory as createBaseHistory} from 'history'
import {atom} from 'derivable'

function createHistory(options) {
  let history = createBaseHistory(options)
  let location = atom(history.getCurrentLocation())
  history.listen(loc => location.set(loc));
  history.location = location.derive(location => location)
  return history
}

let history = createHistory()

Now to build the router we just need to use history.location value in render():

let Router = reactive(props => {
  let {pathname} = history.location.get()
  // Any complex pathname matching logic here, really.
  if (pathname === '/') {
    return <Home />
  } else if (pathname === '/about') {
    return <About />
  } else {
    return <NotFound />
  }
})

Now to change location you would need another component which transforms location state: Link. Also it could track "active" state (if link's location is the current location):

let Link = reactive(props => {
  let {pathname} = history.location.get()
  let className = pathname == props.href ? 'active' : ''
  let onClick = e => {
    e.preventDefault()
    history.push(props.href)
  }
  return <a {...props} onClick={onClick} className={className} />
})

Lifting regular React components to work with derivable values

If you already have a React component which works with regular JS values but want it to work with derivable values you can use this little trick:

import {atom, unpack} from 'derivable'
import {reactive} from 'react-derivable'

class Hello extends React.Component {

  render() {
    return <div>{this.props.message}</div>
  }
}

let ReactiveHello = reactive(props =>
  <Hello {...props} message={props.message.get()} />)

<ReactiveHello message={atom('Hi')} />

Also because you are passing values as plain props they are going to participate in React component lifecycle as usual (e.g. you can access prev values in componentDidUpdate):

class Hello extends React.Component {

  render() {
    return <div>{this.props.message}</div>
  }

  componentDidUpdate(prevProps) {
    if (prevProps.message !== this.props.message) {
      // do something!
    }
  }
}

let ReactiveHello = reactive(props =>
  <Hello {...unpack(props)} />)

Examples

See examples in examples directory.