vigetlabs / microcosm Goto Github PK
View Code? Open in Web Editor NEWFlux 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
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
It is very easy to miss that currying exists. I propose a curry
or prepare
method.
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?
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!
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.
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
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
It took me while to find out the following steps:
npm install
npm run-script start
These instructions are actually already in the CONTRIBUTING.md file. But I think this is not the best place for new users.
in 9.0.0-beta-3
optimistic updates came using generators.
I think we should address reject
ing 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
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.
I want to be able to "fork" a microcosm, adding new stores but sharing the same action history tree.
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.
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.
This github issue summarizes the plan for Microcosm 10.x:
app.push
Actions have 4 states:
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
}
}
}
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.
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.
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?
}
}
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.
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
Specifically this addresses the use case:
let record= app.pull('blockTypes', i => i.filter(i.id === id))
Maybe give each plugin a name?
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?
Current thoughts:
https://gist.github.com/nhunzaker/81aa65a6a7b496a2e6ca
We should provide a consistent way to determine when an action has fully resolved. The current proposal is:
app.push(action, [ arguments ], callback)
We should improve the error message for when propMap[key] is undefined inside of Connect
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.
Can save some headaches.
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.
We'd need to build this into Foliage, but this would give us really fine grained control over change management for relatively cheap.
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.
If a Connect
instance is used within another Connect
instance we should confirm that the React component tree only renders once. This might be happening, however test coverage would be an excellent upgrade.
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?
Basically, we should support:
Connect(undefined)
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?
In our guides, we use console.log in a few places. We should add a little comment on the side of them that lets the reader know what they should see.
Thanks, @cwmanning
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:
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)
)
}
}
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)
}
}
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.
I would delete this if I could. I have no issue.
Microcosm is mostly pure. There isn't any reason we couldn't MapReduce stores.
What if we checked the arity of the register function and let them work without calling next()
?
We should explain the difference between:
app.addStore('key', Store)
and
app.addStore(Store)
Specifically how this works, and how they will write to app.state
Maybe:
module.exports = React.createClass({
render() {
return <div className="screenreader-only" aria-live="assertive">
{this.props.announcement}
</div>
}
})
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:
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.
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);
We've got a pretty good example. Let's write a guide for how to make it.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.