Giter VIP home page Giter VIP logo

microcosm's Introduction

Microcosm

CircleCI npm npm

Microcosm is a state management tool for React (and similar libraries). Keep track of user actions, cancel requests, and perform optimistic updates with ease.

Heads up

As of July 2019, Microcosm is no longer in active development. We will continue to maintain it for existing clients, and would be happy to accept contributions to continue support in the future.

It's been a great journey, thanks for taking it with us.

What you get

At a glance

import Microcosm, { get, set } from 'microcosm'
import axios from 'axios'

let repo = new Microcosm()

function getUser (id) {
  // This will return a promise. Microcosm automatically handles promises.
  // See http://code.viget.com/microcosm/api/actions.html
  return axios(`/users/${id}`)
}

// Domains define how a Microcosm should turn actions into new state
repo.addDomain('users', {
  getInitialState () {
    return {}
  },
  addUser (users, record) {
    // The set helper non-destructively assigns keys to an object
    return set(users, record.id, record)
  },
  register () {
    return {
      [getUser]: {
        done: this.addUser
      }
    }
  }
})

// Push an action, a request to perform some kind of work
let action = repo.push(getUser, 2)

action.onDone(function () {
  let user = get(repo.state, ['users', '2'])

  console.log(user) // { id: 2, name: "Bob" }
})

// You could also handle errors in a domain's register method
// by hooking into `getUser.error`
action.onError(function () {
  alert("Something went terribly wrong!")
})

Why?

Other Flux implementations treat actions as static events; the result of calling a dispatch method or resolving some sort of data structure like a Promise.

But what if a user gets tired of waiting for a file to upload, or switches pages before a GET request finishes? What if they dip into a subway tunnel and lose connectivity? They might want to retry a 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. Presentation layer requirements leak into the data layer, making it harder to write tests, reuse code, and accommodate unexpected changes.

How Microcosm is different

Microcosm actions are first-class citizens. An action can move from an open to error state if a request fails. Requests that are aborted may move into a cancelled state. As they change, actions resolve within a greater history of every other action.

This means that applications can make a lot of assumptions about user actions:

  • Actions resolve in a consistent, predictable order
  • Action types are automatically generated
  • Actions maintain the same public API, no matter what asynchronous pattern is utilized (or not)

This reduces a lot of boilerplate, however it also makes it easier for the presentation layer to handle use-case specific display requirements, like displaying an error, performing an optimistic update, or tracking file upload progress.

Get started

npm install --save microcosm

Check out our quickstart guide.

Documentation

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

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:

// axios is an AJAX library
// https://github.com/mzabriskie/axios
import axios from 'axios'

function getPlanet (id) {
  // axios returns a Promise, handled out of the box
  return axios(`/planets/${id}`)
}

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,
        update : this.setProgress,
        done   : this.addPlanet,
        error  : this.setError,
        cancel : 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 simpler 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 {
      [send]: {
        open: this.setPending,
        error: this.setError,
        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)
  }

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

  render () {
    const { page } = this.model

    return <UsersTable users={page} />
  }
}

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

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

Inspiration


Code At Viget

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

microcosm's People

Contributors

cwmanning avatar despairblue avatar greypants avatar huyb1991 avatar ipbrennan90 avatar leobauza avatar mackermedia avatar nhunzaker avatar prayash avatar samatar26 avatar solomonhawk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

microcosm's Issues

Adding more than one store to repo causes errors when actions are pushed

This is probably another PEBKAC error, but I'm having trouble in my app as soon as I add a second store to the repo with Microcosm v10.0.0-beta3:

Results:

bundle.js:1596 Uncaught Error: When dispatching the list1 action's open state to the twos store, we encountered an "undefined" attribute within register(). This usually happens when an action is imported from the wrong namespace, or by referencing an invalid action state.

When I comment unused addStore calls, the problem clears up.

Code:

import Microcosm from 'microcosm';
import axios from 'axios';

/******************************************************************
 * Actions
 * ***************************************************************/

function list1 () {
    return axios.get('https://jsonplaceholder.typicode.com/posts');
}

function list2 () {
    return axios.get('https://jsonplaceholder.typicode.com/posts');
}

function list3 () {
    return axios.get('https://jsonplaceholder.typicode.com/posts');
}


/******************************************************************
 * Stores
 * ***************************************************************/

const Store1 = {
    getInitialState() {
        return '0';
    },

    register() {
        return {
            [list1.open]: this.list1Loading,
            [list1.done]: this.list1Completed,
            [list1.failed]: this.list1Failed,
            [list1.cancelled]: this.list1Cancelled
        };
    },

    list1Loading(state) {
        console.log(`list1Loading received ${state} and state is actually ${app.state.widgets}.`);

        return 'a';
    },

    list1Cancelled(state) {
        console.log(`list1Cancelled received ${state} and state is actually ${app.state.widgets}.`);

        return 'b';
    },

    list1Completed(state) {
        console.log(`list1Completed received ${state} and state is actually ${app.state.widgets}.`);

        return 'c';
    },

    list1Failed(state) {
        console.log(`list1Failed received ${state} and state is actually ${app.state.widgets}.`);

        return 'd';
    }
};

const Store2 = {
    getInitialState() {
        return '0';
    },

    register() {
        return {
            [list2.open]: this.list2Loading,
            [list2.done]: this.list2Completed,
            [list2.failed]: this.list2Failed,
            [list2.cancelled]: this.list2Cancelled
        };
    },

    list2Loading(state) {
        console.log(`list2Loading received ${state} and state is actually ${app.state.widgets}.`);

        return 'a';
    },

    list2Cancelled(state) {
        console.log(`list2Cancelled received ${state} and state is actually ${app.state.widgets}.`);

        return 'b';
    },

    list2Completed(state) {
        console.log(`list2Completed received ${state} and state is actually ${app.state.widgets}.`);

        return 'c';
    },

    list2Failed(state) {
        console.log(`list2Failed received ${state} and state is actually ${app.state.widgets}.`);

        return 'd';
    }
};

const Store3 = {
    getInitialState() {
        return '0';
    },

    register() {
        return {
            [list3.open]: this.list3Loading,
            [list3.done]: this.list3Completed,
            [list3.failed]: this.list3Failed,
            [list3.cancelled]: this.list3Cancelled
        };
    },

    list3Loading(state) {
        console.log(`list3Loading received ${state} and state is actually ${app.state.widgets}.`);

        return 'a';
    },

    list3Cancelled(state) {
        console.log(`list3Cancelled received ${state} and state is actually ${app.state.widgets}.`);

        return 'b';
    },

    list3Completed(state) {
        console.log(`list3Completed received ${state} and state is actually ${app.state.widgets}.`);

        return 'c';
    },

    list3Failed(state) {
        console.log(`list3Failed received ${state} and state is actually ${app.state.widgets}.`);

        return 'd';
    }
};

/******************************************************************
 * Bootstrapping
 * ***************************************************************/

class App extends Microcosm {
    constructor() {
        super();

        this.addStore('ones', Store1);
        this.addStore('twos', Store2);
        this.addStore('threes', Store3);
    }
}

const app = new App();


/******************************************************************
 * Bootstrapping
 * ***************************************************************/

app.push(list1);
setTimeout(() => {
    app.push(list2);

    setTimeout(() => {
        app.push(list3);
    }, 1000);
}, 1000);

Autobind action operations

It would be nice to auto-bind action.close, action.reject, etc..., so that they can be passed around easier, like:

import promiseAjax from 'somewhere'

function searchAction (query) {
  return function (action) {
    action.open()
    promiseAjax.get('/search', query).then(action.close, action.reject)
  }
}

State supplied to action lifecycle hooks is stale

We're running into some confusion with Microcosm v10.0.0-beta3 and I'm unsure if I'm using the library incorrectly or perhaps it's just a symptom of playing with beta software. Strangely, I don't seem to see this behavior in the examples. Thanks in advance for any guidance!

Results:

listLoading received 0 and state is actually 0.
listCompleted received 0 and state is actually a.
listLoading received c and state is actually c.
listCompleted received c and state is actually a.
listLoading received c and state is actually c.
listCompleted received c and state is actually a.

I expected the received and actual values to match.

Code:

import Microcosm from 'microcosm';
import axios from 'axios';

/******************************************************************
 * Action
 * ***************************************************************/

function list () {
    return function (action) {
        action.open();

        let cancelled = false;
        action.on('cancel', () => cancelled = true);

        axios.get('https://jsonplaceholder.typicode.com/posts')
            .then(response => {
                if (!cancelled) action.close(response);
            })
            .catch(response => {
                if (!cancelled) action.reject(response);
            });
    }
}


/******************************************************************
 * Store
 * ***************************************************************/

const Store = {
    getInitialState() {
        return '0';
    },

    register() {
        return {
            [list.open]: this.listLoading,
            [list.done]: this.listCompleted,
            [list.failed]: this.listFailed,
            [list.cancelled]: this.listCancelled
        };
    },

    listLoading(state) {
        console.log(`listLoading received ${state} and state is actually ${app.state.widgets}.`);

        return 'a';
    },

    listCancelled(state) {
        console.log(`listCancelled received ${state} and state is actually ${app.state.widgets}.`);

        return 'b';
    },

    listCompleted(state) {
        console.log(`listCompleted received ${state} and state is actually ${app.state.widgets}.`);

        return 'c';
    },

    listFailed(state) {
        console.log(`listFailed received ${state} and state is actually ${app.state.widgets}.`);

        return 'd';
    }
};


/******************************************************************
 * Bootstrapping
 * ***************************************************************/

class App extends Microcosm {
    constructor() {
        super();

        this.addStore('widgets', Store);
    }
}

const app = new App();


/******************************************************************
 * Run action
 * ***************************************************************/

app.push(list);
setTimeout(() => {
    app.push(list);

    setTimeout(() => {
        app.push(list);
    }, 1000);
}, 1000);

Runnable repo here (npm install && npm start): https://github.com/Torchlite/microcosm-test

Dev tools

We're in the middle of a couple of interesting ideas related to undo/redo and transactional state. More concrete examples would help to flesh out ideas. Writing a chrome dev tool for rewinding state for debugging purposes and recording store operations could be a great opportunity for this.

Optimistic updates with promises

in 9.0.0-beta-3 optimistic updates came using generators.
I think we should address rejecting a Promise too, to cancel the now-not-so-optimistic update.
what do you think?

I don't know yet how, but I know that it has to be treated

Microcosm 10.x

This github issue summarizes the plan for Microcosm 10.x:

  1. Externalize action resolution state
  2. No more generators
  3. Return a common emitter from app.push
  4. Actions are cancellable
  5. Stores return instructions, state management is handled by an adapter

Externalize action resolution state

Actions have 4 states:

  1. loading: The action was pushed, but no updates have occurred
  2. progress: The action has emitted an update, but is not complete
  3. complete: The action has completely resolved
  4. failed: An error occurred resolving the action (such as a 500 HTTP status)

In practice, stores register to these states by accessing properties on action functions themselves:

function getPlanets () {
    return request("/planets")
}

const PlanetsStore = {
    // ...handlers....
    register() {
        return {
            [getPlanets.loading]  : PlanetsStore.markLoading,
            // This is functionally the same as `[getPlanets]: PlanetsStore.append`
            [getPlanets.complete] : PlanetsStore.append
        }
    }
}

No more generators

The generator pattern used in prior versions of Microcosm to assist with optimistic updates is brittle and only supports very specific use cases.

Specifically, I believe actions themselves should not manage their resolution state. An action should not be responsible for indicating that it is loading, or in progress. Let the underlying mechanisms (such as streams, or promises) manage that. Microcosm should provide a layer between actions and their yield values such that the user subscribes to action resolution state, instead of managing it themselves.

Additionally, the new method described in the previous section should account for all use cases for the generator action pattern and allow for more flexible usage.

Return a common emitter from app.push

In the current Microcosm, the third argument of app.push is a callback function that executes when an action closes (be it resolution or rejection):

app.push(action, [ 'arg-one', 'arg-two' ], function (error) {
    // executed after Microcosm "rolls forward" with the new state. Always.
})

There are a lot of nice guarantees here. However the ergonomics of it aren't great. Particularly with app.prepare, the internal mechanics are awkward, and it is counter-intuitive to use an array to send multiple arguments into an action.

Instead, **I propose dropping the callback argument and returning an event emitter that conforms to the NodeJS streams API:"

app.push(action, 'arg-one', 'arg-two').on('end', function (error, body) {
    // ...
})

Whether or not the streams API is the correct API to confirm to is an outstanding question. However the core idea is to return an event emitter.

Actions are cancellable

Within the model described in the prior section, app.push returns the actual transaction that represents the resolution of an action (is transaction a good name?).

We should expose hooks to allow for the cancellation of actions. Specifically to address actions that a particular presentation layer view may have a dependency on. For example: a search page. When the user leaves the search page, or enters a new search, we no longer care about the old request. Microcosm should provide a consistent way to cancel requests:

_myEventCallback() {
    this.request = app.push(action, 'one', 'two')
},

componentWillUnmount() {
    if (this.request) {
        this.request.abort() // or cancel?
    }
}

Stores return instructions, state management is handled by an adapter

We historically keep track of all actions that are pushed. Why can't we do this for state as well? Additionally, would it be possible to expose this in a way that makes it easy to write tests? Instead of checking against state, could you simply check that the right commands were setup?

Out of all these new features, this is probably the least well defined. It's also heavily inspired by Ecto's Changesets, but possibly:

const Planets = {

    getInitialState() {
        return [ 'reset', 'planets' ]
    },

    add(params) {
        return [ 'insert', `planets/${ params.id }`, params ]
    },

    update(params) {
        return [ 'update', `planets/${ params.id }`, params ]
    },

    remove(id) {
        return [ 'remove', `planets/${ id }` ]
    }
}

There are definite flaws here, but the core pain point this addresses is the frustration with manually performing immutable updates. If we suggest (or even expect) immutability in Microcosm, it needs to be the default.

Lifecycle methods?

Just wanted to open an issue to think through a some potential hooks. When an action resolves? Before/after a dispatch? When a microcosm resets or replaces?

Transactions should probably be observables

For background, whenever an action is pushed into Microcosm via app.push, it runs coroutine on the value returned from the action. There are 3 states returned from coroutine: progress, fail, and done. This is very similar to how Observables work, and I would love to expose interoperability with transactions to libraries like Rx. At the very least, use an already existing pattern that is well established in the community.

If we did this, is it feasible to make it compliant with RxJS? https://github.com/Reactive-Extensions/RxJS. We have our own scheduler, dispose strategy, and observation pattern - would it be too hard do this? How could we mitigate as many breaking changes as possible?

examples throw error

all examples throw

invariant.js?96aa*:42 Uncaught Error: Invariant Violation: _registerComponent(...): Target container is not a DOM element.
invariant   @   invariant.js?96aa*:42
ReactMount._registerComponent   @   ReactMount.js?2450*:355
ReactMount._renderNewRootComponent  @   ReactMount.js?2450*:395
ReactPerf.measure.wrapper   @   ReactPerf.js?6da6*:70
ReactMount.render   @   ReactMount.js?2450*:493
ReactPerf.measure.wrapper   @   ReactPerf.js?6da6*:70
(anonymous function)    @   index.jsx:35
(anonymous function)    @   Microcosm.js?0648*:258
(anonymous function)    @   Microcosm.js?0648*:257
install @   install.js?d8f2*:8
start   @   Microcosm.js?0648*:256
exports.default.obj.__esModule.default  @   index.jsx:29
__webpack_require__ @   bootstrap a628ef5f159c41ff2674?2f37*:19
(anonymous function)    @   bootstrap a628ef5f159c41ff2674?2f37*:39
(anonymous function)    @   bootstrap a628ef5f159c41ff2674?2f37*:39

Pass repo as second argument of "thunk" action type.

I was against it before, but I think we probably want to pass repo as the second argument of the thunk action type. Having access to the prior history inside of an action lets you do really cool things.

My first concern is that users will be compelled to call repo.push inside of another action. Maybe we could put a warning here (or maybe it's not a bad thing?)

Either way, I'll start enumerating use cases:

Automatic cancellation

import ajax from 'somewhere'

function search (query) {
  return function (action, repo) {
    // Just an idea, this isn't implemented. Cancel all
    // outbound searches
    repo.cancel(search)

    action.open()

    return ajax.get('/search', query).then(
      data => action.close(data), 
      error => action.reject(error)
    )
  }
}

Thoughts on a getDataBindings method for React components?

I was wondering if you'd thought of providing finer grained subscriptions to data change events?
As it stands the current approach

let app = new Microcosm()

// Add a callback
app.listen(callback)

would require a complete render of your entire application from the root React component.
This works for simple applications but once you start buildings larger applications or have a high
frequency of change events it will struggle.

Have you seen how nuclear-js handles this case? It has a getDataBindings method on the
React components that specify what the components are interested in. This is similar to GraphQL.

The approach of having view controller components declare what data they need seems a sensible
one.

Merge examples and site

Instead of having a static site, let's make the root index.html of the examples folder the site itself. The site doesn't have to change a bunch, just list the examples at the bottom.

Register function

In the past, we had a register function on stores that provided some extra configuration options around how a Store should respond to actions. Let's consider adding that back

What if we called Microcosm instances `repo` instead of `app`?

var app = new Microcosm() has been gnawing at me for a while. Particularly inside of Presenter intent callbacks, I really like how this reads:

class PlanetsPresenter extends Presenter {

  onCreate(repo, props) {
    return repo.push(createPlanet, props)
  }

  // ...
  render() {
    return 
  }

}

Also open to other suggestions. store came to mind, but that's not really accurate.

Umbrella apps

I want to be able to "fork" a microcosm, adding new stores but sharing the same action history tree.

Use case 1

Let's say we have a table view, where a user can browser through a paginated table of people, showing their information in a detail panel:

+------[application shell]------+
| +---[table]---+ +-[detail]--+ |
| |             | |           | | 
| |             | |           | |   
| |             | |           | |     
| |             | |           | |       
| +-------------+ +-----------+ | 
+-------------------------------+

We keep track of all people in a people store. We absolutely need pagination for something like this. There may be thousands of records.

But we can't simply load a new page and wipe away the old data. What if the person we are looking at in the detail panel isn't included in the current page?

My present solution is to store all people in the people store, and assign state within the Presenter that loads the data like:

class People extends Presenter {
  setup(repo, props) {
    repo.push(getPeople, { page: props.query.page }).onDone(data => {
      this.setState({ page: data.map(person => person.id) })
    })

    viewModel() {
      return { people: state => state.people }
    }

    render() {
      let currentPeople = this.state.people.filter(p => this.state.page.indexOf(p.id) >= 0)

      return <Table people={currentPeople}/>
    }
  }

So this is okay, but kind of crufty. I'd much rather just have a store local to the presenter that keeps track of the current page. There's less state to keep track of, and testing becomes easier because you just have to test that the store wipes away the old page, and that the presenter pushes the action.

Basically:

var repo = new Microcosm()

repo.addStore('people', People)

var fork = repo.fork()

// Maybe we could figure out how to reuse a general 
// purpose pagination store across multiple resources
fork.addStore('page', PaginatedPeople)

fork.push(getPeople)
// 1. Action is pushed into the shared history for the repo and the fork
// 2. Stores in repo process action
// 3. Stores in fork process action
// 4. "page" key in fork does not propagate changes to the repo

This is cool because it lets us share the same pool of resources. We don't have to duplicate data. Also, we never have to work about the AJAX request for an individual person loading before the table view, with the table view blowing away all of the data.

Use case 2

On a current project, we have a use case where two UI components manage the same information in fundamentally different ways.

A good example of this is a 7 degrees of separation to Kevin Bacon app.

+------[application shell]------+
| +-[list]-+ +---[data viz]---+ |
| |        | |                | | 
| |        | |                | |   
| |        | |                | |     
| |        | |                | |       
| +--------+ +----------------+ | 
+-------------------------------+

On the left, we want to show a list of all people we know about (including Mr Bacon). On the right, we want to show a super fancy HTML5 canvas data visualization that is extremely performance intensive.

Naturally, on the left we want to show all of the people in a list, so we prepare a people store that manages information like:

[
  { id: 'david-blain', name: "David Blain" },
  { id: 'kevin-bacon', name: "Kevin Bacon" },
 //...
]

HTML5 canvas means we have to take a more hands on approach to updating the data viz. React's model of "rewrite the app every time" breaks down. Our solution is to only update the data visualization in patches, like:

[ "add", "david-blain", { x: 200, y: 200 },
  "add", "kevin-bacon", { x: 100, y: 30 },
  "connect" "kevin-bacon", "david-blain"]

So easy enough, we could just make a people store that manages both data forms. But the data-viz format is only needed for the data visualization. No need to force updates on other children as that data changes.

Callbacks for app.push

We should provide a consistent way to determine when an action has fully resolved. The current proposal is:

app.push(action, [ arguments ], callback)

Remove connect and provider addons

Presenter has made both connect and provider empty shells:

// Provider addon:
class Provider extends Presenter {}
// Connect addon:
export default function connect (computer, options) {

  return function wrapWithConnect(Component) {

    class Connect extends Presenter {
      viewModel(props) {
        return computer ? computer(props) : {}
      }
      render() {
        const props = merge({ repo: this.repo }, this.props, this.state)
        return React.createElement(Component, props)
      }
    }

    Connect.defaultProps     = options
    Connect.displayName      = `Connect(${Component.displayName || Component.name || 'Component'})`
    Connect.WrappedComponent = Component

    return hoistStatics(Connect, Component)
  }
}

What do you all think about removing them?

Rename store register to receive?

The store's register method doesn't really register anything: it declares how a store should respond when it receives an action. Should we consider updating this in a major release?

What is the best way to handle errors when seeding data?

This has come up specifically when localStorage or a text input contains a stale version of a schema and the app expects another.

Right now this is being handled a layer above, in the application. I wonder what tools we can provide to make this less painful.

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.