Giter VIP home page Giter VIP logo

microcosm's Introduction

Microcosm

CircleCI Coveralls npm npm

Microcosm is Flux with first-class actions and state sandboxing.

The source of truth in Microcosm is a historical record of actions. As they move through a set lifecycle, Microcosm reconciles actions in the order they were created. This makes optimistic updates, cancellation, and loading states much simpler. They self clean.

It also provides strong separation between actions and state. Microcosms can "fork" to accommodate specific use cases. Keep global state global, while easily supporting pagination, filtering, and other secondary data processing.

  1. Batteries included. Just install microcosm. Plugins and middleware should not be required to immediately be productive.
  2. Easy to use. Less boilerplate. More expressive. The Microcosm interface is easy to use, easy to understand, and easy to maintain.
  3. Powerful: Microcosm's transactional state management, action statuses, and state sandboxing provide exceptional tools for building applications.

Documentation

Comprehensive documentation can be found in the docs section of this repo.

If you'd rather look at working code, head over to the example apps or checkout out the quickstart guide.

Installation

npm install --save microcosm

Overview

Microcosm is an evolution of Flux that makes it easy to manage complicated async workflows and unique data modeling requirements of complicated UIs.

Actions take center stage

Microcosm organizes itself around a history of user actions. As those actions move through a set lifecycle, Microcosm reconciles them in the order they were created.

Invoking push() appends to that history, and returns an Action object to represent it:

function getPlanet (id) {
  // Fetch returns a Promise, handled out of the box
  return fetch('/planets/' + id).then(response => response.json())
}

let action = repo.push(getPlanet, 'venus')

action.onDone(function (planet) {
  console.log(planet.id) // venus
})

Domains: Stateless Stores

A Domain is a collection of side-effect free operations for manipulating data. As actions update, Microcosm uses domains to determine how state should change. Old state comes in, new state comes out:

const PlanetsDomain = {
  getInitialState () {
    return []
  },

  addPlanet (planets, record) {
    return planets.concat(record)
  },

  register() {
    return {
      [getPlanet]: this.addPlanet
    }
  }
}

repo.addDomain('planets', PlanetsDomain)

By implementing a register method, domains can subscribe to actions. Each action is assigned a unique string identifier. Action type constants are generated automatically.

Pending, failed, and cancelled requests

Microcosm makes it easy to handle pending, loading, cancelled, completed, and failed requests:

const PlanetsDomain = {
  // ...handlers

  register() {
    return {
      [getPlanet.open]      : this.setPending,
      [getPlanet.done]      : this.addPlanet,
      [getPlanet.error]     : this.setError,
      [getPlanet.loading]   : this.setProgress,
      [getPlanet.cancelled] : this.setCancelled
    }
  }
}

open, loading, done, error and cancelled are action states. In our action creator, we can unlock a deeper level of control by returning a function:

import request from 'superagent'

function getPlanet (id) {

  return function (action) {
    action.open(id)

    let request = request('/planets/' + id)

    request.end(function (error, response) {
      if (error) {
        action.reject(error)
      } else {
        action.resolve(response.body)
      }
    })

    // Cancellation!
    action.onCancel(request.abort)
  }
}

First, the action becomes open. This state is useful when waiting for something to happen, such as loading. When the request finishes, if it fails, we reject the action, otherwise we resolve it.

Microcosm actions are cancellable. Invoking action.cancel() triggers a cancellation event:

let action = repo.push(getPlanet, 'Pluto')

// Wait, Pluto isn't a planet!
action.cancel()

When action.cancel() is called, the action will move into a cancelled state. If a domain doesn't handle a given state no data operation will occur.

Visit the API documentation for actions to read more.

A historical account of everything that has happened

Whenever an action creator is pushed into a Microcosm, it creates an action to represent it. This gets placed into a tree of all actions that have occurred.

For performance, completed actions are archived and purged from memory, however passing the maxHistory option into Microcosm allows for a compelling debugging story, For example, the time-travelling Microcosm debugger:

let forever = new Microcosm({ maxHistory: Infinity })
Microcosm Debugger

Taken from the Chatbot example.

Optimistic updates

Microcosm will never clean up an action that precedes incomplete work When an action moves from open to done, or cancelled, the historical account of actions rolls back to the last state, rolling forward with the new action states. This makes optimistic updates a sync because action states are self cleaning:

import {send} from 'actions/chat'

const Messages = {
  getInitialState () {
    return []
  },

  setPending(messages, item) {
    return messages.concat({ ...item, pending: true })
  },

  setError(messages, item) {
    return messages.concat({ ...item, error: true })
  },

  addMessage(messages, item) {
    return messages.concat(item)
  }

  register () {
    return {
      [action.open]  : this.setPending,
      [action.error] : this.setError,
      [action.done]  : this.addMessage
    }
  }
}

In this example, as chat messages are sent, we optimistically update state with the pending message. At this point, the action is in an open state. The request has not finished.

On completion, when the action moves into error or done, Microcosm recalculates state starting from the point prior to the open state update. The message stops being in a loading state because, as far as Microcosm is now concerned, it never occured.

Forks: Global state, local concerns

Global state management reduces the complexity of change propagation tremendously. However it can make application features such as pagination, sorting, and filtering cumbersome.

How do we maintain the current page we are on while keeping in sync with the total pool of known records?

To accommodate this use case, there is Microcosm::fork:

const UsersDomain = {
  getInitialState() {
    return []
  },
  addUsers(users, next) {
    return users.concat(next)
  },
  register() {
    return {
      [getUsers]: this.addUsers
    }
  }
})

const PaginatedUsersDomain {
  getInitialState() {
    return []
  },
  addUsers(users, next) {
    let page = next.map(user => user.id)

    // Reduce the user list down to only what was included
    // in the current request
    return users.filter(user => page.contains(user.id))
  },
  register() {
    return {
      [getUsers]: this.addUsers
    }
  }
})

let roster = new Microcosm()
let pagination = parent.fork()

roster.addDomain('users', UsersDomain)
pagination.addDomain('users', PaginatedUsersDomain)

// Forks share the same history, so you could also do
// `pagination.push(getUsers, ...)`
roster.push(getUsers, { page: 1 }) // 10 users
roster.push(getUsers, { page: 2 }) // 10 users

// when it finishes...
console.log(roster.state.users.length) // 20
console.log(pagination.state.users.length) // 10

fork returns a new Microcosm, however it shares the same action history. Additionally, it inherits state updates from its parent. In this example, we've added special version of the roster repo that only keeps track of the current page.

As getUsers() is called, the roster will add the new users to the total pool of records. Forks dispatch sequentially, so the child pagination repo is able to filter the data set down to only what it needs.

Networks of Microcosms with Presenters

Fork is an important component of the Presenter addon. Presenter is a special React component that can build a view model around a given Microcosm state, sending it to child "passive view" components.

All Microcosms sent into a Presenter are forked, granting them a sandbox for data operations specific to a particular part of an application:

class PaginatedUsers extends Presenter {
  setup (repo, { page }) {
    repo.add('users', PaginatedUsersDomain)

    repo.push(getUsers, page)
  }

  model () {
    return {
      page: state => state.users
    }
  }

  view ({ page }) {
    return <UsersTable users={page} />
  }
}

const repo = new Microcosm()
repo.addDomain('users', UsersDomain)

ReactDOM.render(<PaginatedUsers repo={repo} page="1" />, el)

Why another Flux?

Good question! Other popular implementations of Flux treat actions as static events. The result of calling a dispatch method or resolving some sort of data structure like a Promise.

But what about everything before that point? A user might get tired of waiting for a file to upload, or dip into a subway tunnel and lose connectivity. They might want to retry an request, cancel it, or just see what’s happening.

The burden of this state often falls on data stores (Domains, in Microcosm) or a home-grown solution for tracking outstanding requests and binding them to related action data.

While manageable, we’ve found that this can be cumbersome. That it can lead to interface-specific requirements leaking into the data layer, resulting in complicated code, and unexpected bugs as requirements change.

How Microcosm is different

Microcosm thinks of actions as stories. They go through different states as they move from start to completion. Actions have a common public interface, regardless of what data structures or asynchronous patterns are utilized. An interface that is easy to query from the presentation layer in order to handle use-case specific display requirements.

This makes it easier to handle complicated behaviors such as optimistic updates, dialog windows, or long running processes.

Inspiration


Code At Viget

Visit code.viget.com to see more projects from Viget.

microcosm's People

Contributors

despairblue avatar greypants avatar nhunzaker avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.