-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Any plans to support Om-like reference cursors? #42
Comments
Hi! You can do this with immstruct and Immutable.js as well, you can create a cursor based on a path, and immstruct will fix so these cursors doesn't get stale (and thus avoid losing data). Let's say you have a file with your structure: var immstruct = require('immstruct');
module.exports = immstruct({
/* My large Structure */
}); And in some component var structure = require('./myStructure.js');
var SomeSubComponent = component(({cursor}) => (<div>{cursor.deref()}</div>));
var SomeComponent = component(({cursor}) => {
var newCursor = structure.cursor(['path', 'to', 'someWhere']);
return <SomeSubComponent cursor={newCursor} />;
}); Or are you thinking about setting a cursor on a specific component? Something like this: var structure = require('./myStructure.js');
var SomeSubComponent = component({
cursor: structure.cursor(['path', 'to', 'someWhere'])
}, ({cursor}) => (<div>{cursor.deref()}</div>));
var SomeComponent = component(({cursor}) => {
return <SomeSubComponent />;
}); Something like that could possibly work, but it defeats the purpose of having re-usable and composable components. |
This is a contrived example, but bear with me: Imagine this hypothetical large structure for a quiz application:
Also imagine that in your first example, In this case, you have two different components pointing to completely separate parts of the structure, but one is embedded in the other. Because of how This is what something like a reference cursor would aim to solve. Taken from the link I mentioned, the problem is that:
Perhaps something like your second example could work. And I'm not sure that I agree that it would defeat the purpose of having re-usable and composable components. If a component knew only about the data it needed, didn't need to receive data from it's parent and didn't need to pass data down to components below it, it would be very composable because it could essentially just live completely on its own and be "put" anywhere. Right? |
Another thought: It seems like, after talking to David Nolen, how this is implemented in Om may not be possible in Omniscient using the same methods. What Om does is, in it's render function, loops through all components, checks their This is, of course, different than what's going on with immstruct swap and Omniscient. In omniscient, we're just blindly rendering the whole tree, relying on Not sure what we could do about this if this functionality is something that could be added to omniscient. I'll put some thought into it. |
I see what you're getting at, and as of today this won't really work without both components being passed a cursor to the piece of state that encompasses them both ( While I absolutely agree that something similiar could be useful in some scenarios, I'm also kind of reluctant, as it complicates reasoning about what renders when. Are we not in this way effectively relying on "some global state" to also trigger re-renders of some sub component? :) In some cases this is probably exactly what you'd want, though. I haven't really dug into Om, but is it tracking individual components to accomplish this - to re-render only parts of the component tree that rely on this data without starting from the top? Or could we use a mixin that var SubView = component([ forceUpdateOnChange(items) ], () => ...)
var MainView = component(({someOtherCursor}) =>
<div>
<SubView />
...
</div>); We've been on to vaguely similar ideas with immstruct for reacting to changes of these kinds of structures omniscientjs/immstruct#19 and omniscientjs/immstruct#18 |
Short answer - yes, I think a mixin can work, at least for a naive implementation, but I don't think we'd be able to get the same performance improvements that Om get's when dealing with reference cursors. I played around with this a bit last night. Basically, I just had mixin (like you proposed) that registered the component with a cursor on mount, and then a second listener on structure swap that would handle this "second-render pass". It works. But, what we wouldn't be doing with this method is preventing a component that may have been updated by the first render pass from being rendered again, which Om DOES do (and that's why the reference cursor implementation that they do essentially "costs nothing"). To do that, I think, I'm not sure we would be able to implement anything like that without keeping track of every component (Om does do this...specifically, you can see what it's doing here, if I'm reading the source right (I am by no means a clojure expert): https://github.com/swannodette/om/blob/master/src/om/core.cljs#L1139-L1158) |
Perhaps @swannodette may have insight into why he chose to implement this in Om, and why he feels like it's a good decision and allows for easier, not more complicated, reasoning about UI. Though admittedly, he may not have much more to say than what he's already written. :) I think with this library being called "omniscient", and obviously borrowing many of the ideas from Om, we should definitely consider implementing reference cursors. If we can figure out how to do it efficiently (and with less cruft than Morearty's "context", as most of that functionality already exists in immstruct), I think it would be worthwhile to add it to the library. |
@skevy as was discussed in the React Conference hallway chats the Om approach isn't actually so different from the high level ideas behind Relay - you want to bind a component to some collection of information in the global application state without needing to pass it in from above. This is just a natural organization thing in my opinion. In Om people are not using Reference Cursors for everything, just in the places where it's more natural for the application design. As to why it was implemented in the way it was. Om treats the application component tree like an AST, there's a first pass, and there is a second pass. Nothing happens in the second pass if it was already addressed in the first pass. The Om implementation also permits some further simple optimizations we haven't gotten to yet like sorting components by render depth (again allowing components further up the hierarchy to render first and eliminating later redundant work). |
@skevy I might very well not be grokking this, but why would we need another pass? :)
Will not the default |
I can get why you some time would want to do this, and your example makes sense, @skevy. But it would essentially tightly bind a component to a specific data. I think it's worth trying out the naive approach. I suspect that the performance wouldn't be too bad, and perhaps, as @torgeir says, the |
I suppose the render pass thing would only really matter if you were doing rAF updates, because if we're NOT doing rAF, then multiple renders would happen anyway, which in that case you're correct, the default shouldComponentUpdate would prevent weirdness. If it's batched, however, with rAF (as Om is, if I'm not mistaken), then it would matter. A possible use case that highlights the issue when using rAF is below: var structure = immstruct({
a: { "one": "...", "two": "..." },
b: { "one": "...", "two": "..." }
})
var component1 = component(function({ cursor }) {
return <div>{cursor.deref()}</div>
})
var component2 = component([forceUpdateOnChange(structure.cursor(["b", "one"]))], function({cursor}) {
return <div>{cursor.deref()}, {this.refcursors[0].deref()}</div>
})
var appComponent = component(function({cursor}) {
return (<div>
<component1 cursor=cursor.get("a") />
<component2 cursor=cursor.get("a") />
</div>)
})
function render() {
React.render(<appComponent.jsx cursor={structure.cursor()} />, document.body)
}
structure.on('next-animation-frame', function(newData, oldData, path) {
render()
refCursorPass(path) //A mythical method that doesn't exist yet, but that would loop through registered cursors and call force update on any paths that have partial matches to the updated path
});
structure.cursor().update(['a', 'one'], function(data) {
return "someNewData";
})
structure.cursor().update('b', function(data) {
return data.set("one", "someOtherNewData")
}) If rAF was happening, component2 would update twice in this scenario. Once from the first update (which causes an update because of the passed in cursor) and once from the second update, because of the cursor passed to the "forceUpdate" mixin. |
At this point, I'm not sure if I'm making sense anymore. So if I'm not, please let me know. :-p |
And @mikaelbr, it DOES indeed bind a specific component to a piece of data - that would be the point. As @swannodette says, it's not that you would ALWAYS want to do this, but that sometimes it's better for the overall application design. |
Yeah, absolutely, I get that. But my point is (I was trying to convey – poorly), it could seem as if this would be the exception not the "render norm", and thus, maybe the performance loss isn't too much of an issue. |
Something like |
@mikaelbr Ok. Well, either one works for me to be perfectly honest. So, if we do a ref cursor pass, we need a place to register an object with keys of paths and values of components that our bound to reference cursors. Where do you guys think we could store that? If we do the mixin approach (which honestly I think I like better), we could have the component itself, on componentWillMount, listen to paths in the structure (that are specified through mixin arguments), and then forceUpdate on itself. But that would require a change to immstruct, am I right? |
@skevy the approach you talk about with componentWillMount is the one we plan on trying out, and what I've thought of as the most feasable solutions. As far as I can tell it would require immstruct to have this : omniscientjs/immstruct#18 but for demonstrative purposes it is possible to just do an if check in an on swap listener. I really think doing this as a mixin would be the path of least surprises, and require the least amount of work, for the end user. |
100% agree. Do you guys want me to implement or do you want to handle? |
I think @torgeir is half way through implementing it already, but as it's late evening here he went to bed and planned on continuing tomorrow. But we're always accepting suggestions! |
Alright. Sounds good. Looking forward to seeing what you guys come up with! And I'm very excited to use it. Haha |
I'm really liking the conversation on this, and I've been lightly experimenting on stuff with regards to pretty much the concerns @skevy had. A mixin in tandem with a wrapper around Here's I would do it. I haven't tested this, but I modified it from existing code that instead use store semantics (b/c I'm tired of thinking of trees :P). It requires a library that I call let shouldComponentUpdate = require('omniscient').shouldComponentUpdate;
let updateMixin = {
shouldComponentUpdate: shouldComponentUpdate
};
let alwaysRenderMixin = {
shouldComponentUpdate: () => true
};
let structureMixin = {
contextTypes: {
// TODO: eventually immstruct should be instantiable, so it can be passed
// as a context object. May also be a map of structure instances.
immstruct: React.PropTypes.object
},
structure: function(name, path) {
let structure = this.context.immstruct(name);
if(!path)
return structure;
return structure.cursor(path);
},
listenToCursor: function(structure, path, callback) {
immstructor.listenTo(structure, path, callback);
}
};
module.exports = function component(spec, alwaysRender) {
let _render = spec.render;
spec.render = function() {
return _render.call(this, this.props, this.props.statics);
};
let mixins = alwaysRender ? [alwaysRenderMixin, structureMixin] : [updateMixin, structureMixin];
spec.mixins = _.has(spec, 'mixins') ? mixins.concat(spec.mixins) : mixins;
return React.createClass(spec);
}; |
Half way I don't know, although here's an example of what I was thinking ought to work 😃 https://github.com/torgeir/omniscient-testing-issue-42/blob/master/app.js
Edit: This is wrong. |
I was too fast, this is in fact exactly how it is supposed to work; Here's the same flow with answers updating each 3 seconds and the clock ticking each 1 second spaced for clarity Considerations:
|
@torgeir Interesting. Have you tried updating both |
That ought to work pretty much the same, except the listener for let answers = [];
setInterval(() => {
answers.push(Math.random());
quiz.cursor().update(() => Immutable.fromJS({ answers: answers, clock: new Date() }));
}, 10000);
|
So here is what we need to make this work in a more stable / usable way:
I think this should probably be its own module as it has a hard dependency to |
https://github.com/omniscientjs/omniscient-mixins could need some love, utils for joining mixins with before, after, around semantics etc. should probably go here
Listening to an existing cursor and not a path in side a structure would probably be better, but then the cursor would need to be updated on each render, no? Edit: I'd enjoy let Clock = component('Clock', [ forceUpdateOn(clockCursor) ], () =>
div({},
String(clockCursor.deref()))); |
Indeed, but the difficult part is that we can't have knowledge if it exists other mixins or not, or the order of the possibly existing mixins – within a mixin. If that makes any sense? So it seems to me that there needs to be some kind of logic in omniscient. |
@torgeir Listening to a cursor as an alternative to a |
@mikaelbr I'm thinking this should be left to the user, but we could provide the tools to make it easily possible. |
@dashed I'll see what I can do, should probably come up with a decent example, maybe relate it to the reference-cursors of Om |
I think these hooks need to live on componentDidMount instead of componentWillMount. I ran into a scenario (in server-side rendering) where when this listener was added in componentWillMount, the app state changed between binding that handler in componentWillMount and the component actually being mounted. Something to think about. |
Also, I'm a bit confused by this:
Doesn't React guarantee that lifecycle methods from all mixins are going to be called? From the React docs:
|
Oh, that I didn't know! Certainly makes it less applicable for being part of omniscient core. Some mixin utils are probably handy anyways, though, sometimes you want to ensure the order of calls among several mixins. |
Sure, but I don't think it matters in this case, FWIW. |
Sorry about closing...hit the wrong button! :-p |
If you really want to ensure order, there's something like this: https://github.com/brigand/react-mixin |
Hm. That's actually pretty interesting. This means very much less complexity for the reference cursor implementation. There's no need to ensure order for this mixin. |
@mikaelbr Yah there shouldn't be any need at all. I agree. |
This might be of interest omniscientjs/immstruct#30 (work in progres) |
This is as much as an immstruct issue, rather an omniscient issue. I'm not sure what can be amended to omniscient to assist with this kind of feature. |
As @dashed notes, this is no longer a Omniscient issue. This can be achieved with omniscientjs/immstruct#30 and https://github.com/omniscientjs/omniscient-mixins/blob/master/mixins/forceUpdateOn.js |
https://github.com/swannodette/om/wiki/Advanced-Tutorial#reference-cursors
Supporting these would prevent the long chain of passing props (or in omniscient's case, cursors) down long component trees. It would be really cool to have something like these. Morearty.js has something like this.
Wondering about your thoughts here.
The text was updated successfully, but these errors were encountered: