GithubHelp home page GithubHelp logo

vigetlabs / microcosm Goto Github PK

View Code? Open in Web Editor NEW
487.0 487.0 29.0 8.04 MB

Flux with actions at center stage. Write optimistic updates, cancel requests, and track changes with ease.

Home Page: http://code.viget.com/microcosm/

License: MIT License

JavaScript 91.20% Makefile 0.60% CSS 5.69% HTML 2.33% Shell 0.18%
flux react

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

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?

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

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

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

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.

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.

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.

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

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?

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)

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.

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.

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.

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?

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?

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)
    )
  }
}

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)
  }
}

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.

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);

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.