-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Implement Sideways Data Loading #3398
Comments
|
This is pretty interesting however from the point of view of static typing I'm not so happy of key/value system, their type is pretty much impossible to express. class Foo {
observe() {
return (
loadUser(this.props.userID)
.map(user => { user })
}
render() {
if (this.data.user.id !== this.props.userID) {
// Ensure that we never show inconsistent userID / user.name combinations.
return <Spinner />;
}
return <div>Hello, {this.data.user.name} [{this.props.userID}]!</div>;
}
} And for the case of multiple fetch something like : class Foo {
observe() {
return (
combineLatest(
loadUser(this.props.userID),
loadSomethingElse(this.props.somethingElseId),
(user, somethingElse) => ({ user, somethingElse})
)
}
render() {
..
}
} This is perhaps a bit more verbose, but it allows to have nice static type : interface Comp<T> {
observe(): Observable<T>;
data: T;
} |
Also instead of re executing class Foo {
observe(propsStream) {
return (
propsStream
.flatMap(({ userID }) => loadUser(userId))
.map(user => { user })
);
}
render() {
if (this.data.user.id !== this.props.userID) {
// Ensure that we never show inconsistent userID / user.name combinations.
return <Spinner />;
}
return <div>Hello, {this.data.user.name} [{this.props.userID}]!</div>;
}
} |
The reason is because we don't want to require the use of combinators and understanding RxJS to be able to subscribe to (multiple) Observables. Combining two Observables in this way is quite confusing. In fact, at least for our data sources, we'll probably implement the subscription API but not even include the combinators on the Observables' prototype. That's not a requirement, but you're free to use combinators if you need to. However, to subscribe to a simple Flux store, you shouldn't need to. I think that Flow will probably be able to handle this static type using constraints, but I will check with those guys to make sure. I think that it'll be enough to type the data property and then the observe type can be implied. class Foo extends React.Component {
data : { user : User, content : string };
observe() /* implied as { user : Observable<User>, content : Observable<string> } */ {
}
} This change is not about going all in on Observables as a way to describe application state. That can be implemented on top of this, like you've done before. This is explicitly not about application state since this method is idempotent. The framework is free to unsubscribe and resubscribe as needed. |
cc @ericvicenti |
At least in the case of typescript, there would be no way to constraint the return type of |
I'd love to use this for the next version of React DnD, but obviously this requires waiting for React 0.14. |
Would it be possible to observe Promises? Then one could use a Promises tree to resolve data for the whole component tree before the first render! This would be very useful for server-side React. |
What are the benefits of making this a first-class API? It could essentially be accomplished using a "higher-order component." |
Wrapping in 5 HOCs to get 5 subscriptions is a bit unwieldy and harder to understand for beginners. Understanding I, for one, welcome our new observable overlords. |
I wonder if this can help bring https://github.com/chenglou/react-state-stream closer to React's vanilla API |
Wouldn't it just take one HOC? In the example in your Medium post, you iterate over stores.forEach(store =>
store.addChangeListener(this.handleStoresChanged)
); |
@aaronshaf Depends on use case, for sure. Sometimes it's different kinds of state sources, not just “several stores”. But I can't say on behalf of React team, let's hear what @sebmarkbage says. |
Would love some sort of polyfill to play with this now. I didn't get the idea completely, yet. What is the mechanism involved with dealing with future updates. I'll spend some more time understanding it. I think it should be doable with a simple mixin. |
(@vjeux told me I should chime in! so here I am.) I don't mean to promote my own work, but I think this hook is very similar to the The API looks like: class UserDetails {
getNexusBindings(props) {
return {
// binding to data in the datacenter
posts: [this.getNexus().remote, `users/${this.props.userId}/posts`],
// binding to data in the local flux
mySession: [this.getNexus().local, `session`],
}
}
} The binding is applied/updated during Long story short, you synchronously describe data deps at the component level using a Flux abstraction and the hooks make sure your dependencies are automatically subscribed, injected on updates, and unsubscribed. But it also comes with a nice feature: the exact same lifecycle functions are leveraged to perform data prefetching at server-side rendering time. Basically, starting from the root and recusrively from there, React Nexus pre-fetches the bindings, renders the component, and continues with the descendants until all the components are rendered. |
@aaronshaf @gaearon The benefit of making it first class is:
Besides, I think that best-practice for higher-order components should be to avoid changing the contract of the wrapped component. I.e. conceptually it should be the same props in as out. Otherwise it is confusing to use and debug when the consumer supplies a completely different set of props than is received.
|
@RickWong Yes, it would be fairly trivial to support Promises since they're a subset of Observables. We should probably do that to be unopinionated. However, I would still probably recommend against using them. I find that they're inferior to Observables for the following reasons: A) They can't be canceled automatically by the framework. The best we can do is ignore a late resolution. In the meantime, the Promise holds on to potentially expensive resources. It is easy to get into a thrashy situation of subscribe/cancel/subscribe/cancel... of long running timers/network requests and if you use Promises, they won't cancel at the root and therefore you have to just wait for the resources to complete or timeout. This can be detrimental to performance in large desktop pages (like facebook.com) or latency critical apps in memory constrained environments (like react-native). B) You're locking yourself into only getting a single value. If that data changes over time, you can't invalidate your views and you end up in an inconsistent state. It is not reactive. For a single server-side render that might be fine, however, on the client you should ideally be designing it in a way that you can stream new data to the UI and automatically update to avoid stale data. Therefore I find that Observable is the superior API to build from since it doesn't lock you in to fix these issues if you need to. |
@elierotenberg Thanks for chiming in! It does seem very similar indeed. Same kind of benefits. Do you see any limitations with my proposal? I.e. there something missing, that React Nexus has, which you couldn't build on top of this? Would be nice if we didn't lock ourselves out of important use cases. :) |
From the server-rending standpoint, it is important that we're able postpone the final renderToString until the Observable/Promise has been resolved with data that could be fetched asynchronously. Otherwise, we're still in the position of having to do all asynchronous data fetching outside of React without knowing which components will be on the page yet. I believe react-nexus does allow asynchronous loading to happen within a component before continuing down the render tree. |
Yes, react-nexus explicitly separates:
|
If anybody wants to play around with this kind of API, I made a really dumb polyfill for import React, { Component } from 'react';
export default function polyfillObserve(ComposedComponent, observe) {
const Enhancer = class extends Component {
constructor(props, context) {
super(props, context);
this.subscriptions = {};
this.state = { data: {} };
this.resubscribe(props, context);
}
componentWillReceiveProps(props, context) {
this.resubscribe(props, context);
}
componentWillUnmount() {
this.unsubscribe();
}
resubscribe(props, context) {
const newObservables = observe(props, context);
const newSubscriptions = {};
for (let key in newObservables) {
newSubscriptions[key] = newObservables[key].subscribe({
onNext: (value) => {
this.state.data[key] = value;
this.setState({ data: this.state.data });
},
onError: () => {},
onCompleted: () => {}
});
}
this.unsubscribe();
this.subscriptions = newSubscriptions;
}
unsubscribe() {
for (let key in this.subscriptions) {
if (this.subscriptions.hasOwnProperty(key)) {
this.subscriptions[key].dispose();
}
}
this.subscriptions = {};
}
render() {
return <ComposedComponent {...this.props} data={this.state.data} />;
}
};
Enhancer.propTypes = ComposedComponent.propTypes;
Enhancer.contextTypes = ComposedComponent.contextTypes;
return Enhancer;
} Usage: // can't put this on component but this is good enough for playing
function observe(props, context) {
return {
yourStuff: observeYourStuff(props)
};
}
class YourComponent extends Component {
render() {
// Note: this.props.data, not this.data
return <div>{this.props.data.yourStuff}</div>;
}
}
export default polyfillObserve(YourComponent, observe); |
Is Observable a concrete, agreed upon thing aside from library implementations? What is the contract, is it simple enough to implement without needing to use bacon or Rxjs? As nice as a first class api for sideloading data would be, it seems weird for React to add an api based on an unspeced/very-initial-specing primitive, given React's steady movement towards plain js. Would something like this ties us to a specific user land implementation? as an aside why not Streams? I have no horse in the race, but I am honestly wondering; there is already work done on web streams, and of course there is node |
Another two to consider: https://github.com/cujojs/most and https://github.com/caolan/highland |
@jquense There is active work on a proposal for adding Observable to ECMAScript 7(+) so ideally this would become plain JS. https://github.com/jhusain/asyncgenerator (Currently out-of-date.) We would not take on a dependency on RxJS. The API is trivial to implement yourself without using RxJS. RxJS is the closest to the active ECMAScript proposal. most.js seems doable too. Bacon.js' API seems difficult to consume without taking on a dependency on Bacon because of the use of the The Stream APIs are too high-level and far removed from this use case. |
Is there some kind of “await before render" option? I mean on the client it’s not necessary to wait for all Observables before rendering, but on the server you’d want to wait for them to resolve so that each component's render() is full, not partial.
In all of my explorations I found that this is the most important lifecycle hook missing in server-side React. |
Following up this discussion, I've tried to sum up what React Nexus does in the following post: Ismorphic Apps done right with React Nexus Heres' the diagram of the core prefetching routine: |
👍 this is the big concern for me, thinking about say, promises where implementing your own is extremely fraught unless you know what you're doing. I think otherwise you end up with an implicit requirement on a specific lib in the ecosystem. Tangentially...one of the nice things from the promise world is the A+ test suite, so even across libraries there was at least an assurance of a common functionality of |
Completely agreed. Thankfully observables have a really simple contract, and don't even have built-in methods like |
They might become more complicated (and slower) if the committee insists that calling |
@ccorcos @mitranim For a ready to use Tracker / Vue.js inspired library you could try Mobservable, it observers all data accessed during render, and disposes all subscriptions on unmount (until then the subscriptions are kept alive). We applied it successfully to pretty large projects so far at Mendix. It is pretty unobtrusive to your data as well as it decorates existing objects instead of providing its own model objects. |
@mitranim subscriptions can be permanent. Check this out. sub = Meteor.subscribe('chatrooms')
# this subscription lasts until...
sub.stop() Now there's some interesting stuff with Tracker. Check this out. comp = Tracker.autorun ->
Meteor.subscribe('chatrooms')
# this subscription lasts until...
comp.stop() The last example isnt terribly useful though. Until we do something like this. roomId = new ReactiveVar(1)
comp = Tracker.autorun ->
Meteor.subscribe('messages', roomId.get())
# when I change the roomId, the autorun will re-run
roomId.set(2)
# the subscription to room 1 was stopped and now the subscription to room 2 has started
# the subscription is stopped when I call stop...
comp.stop()
The subscription lasts until the autorun is rerun (a reactive dependency changed) or the computation it lives in stops. Both of which call the computation.onInvalidate hook. Here's a super contrived version that accomplished the exact same thing. Hopefully it will help you understand how Tracker works. And maybe you'll also see how messy it can get. comp = Tracker.autorun ->
Meteor.subscribe('chatrooms')
# is the same as
comp = Tracker.autorun (c) ->
sub = null
Tracker.nonreactive ->
# dont let comp.stop() stop the subscription using Tracker.nonreactive
sub = Meteor.subscribe('chatrooms')
c.onInvalidate ->
# stop the subscription when the computation is invalidated (re-run)
sub.stop()
# invalidate and stop the computation
comp.stop() |
@ccorcos I see the source of confusion now; it was my vocabulary. When talking about subscriptions, I didn't mean Meteor subscriptions. They determine what data is pushed from the server to the client, but aren't relevant to the view layer updates, which is the subject of this discussion. When saying subscription, I was drawing a parallel between traditional event listeners and Tracker's ability to rerun a function that has a dependency on a reactive data source. In the case of React components, that would be a component method that fetches data and then calls @mweststrate Thanks for the example, it looks interesting. |
Ah yes. So they have a clever way of doing it. componentWillMount: function() {
this.comp = Tracker.autorun(() => {
let sub = Meteor.subscribe('messages')
return {
loading: !sub.ready(),
messages: Messages.find().fetch()
}
})
componentWillUnmount: function() {
this.comp.stop()
} The subscription will just resubscribe each time with no issues. sub.ready and Messages.find.fetch are both "reactive" and will trigger the autorun to re-run whenever they change. Whats cool about Tracker is when you start to hide the autoruns and just have in the documentation that a certain function is within a "reactive context" you could throw this in a mixin componentWillMount: function() {
this.comp = Tracker.autorun(() => {
return this.getReactiveData()
})
componentWillUnmount: function() {
this.comp.stop()
} And then you're left with this magically reactive function that just works! getReactiveData: function() {
let sub = Meteor.subscribe('messages')
return {
loading: !sub.ready(),
messages: Messages.find().fetch()
}
} Tracker is pretty cool like this... |
@ccorcos Turns out I was somewhat wrong about automatic unsubs when using Tracker-style eventing. You still need to stop the listener in On a side note, you can create elegant decorators to make component methods reactive. Here's some examples: [1], [2]. Looks like this: export class Chat extends React.Component {
@reactive
updateState () {
this.setState({
auth: auth.read(),
messages: messages.read()
})
}
/* ... */
} |
@mitranim pretty neat -- I prefer high-order functions though. ;) |
@sebmarkbage @jimfb I've been following this thread and the alt thread (#3858) for a few months now, and am curious if the core team has reached any consensus about this problem, or at least a general direction. |
@oztune No updates; we've been focused on other priorities. We will post to one of the threads when there is an update on this topic. |
Guys, I have a created a universal API to compose containers. Check react-komposer. Which that, we could create containers with a higher order function. import { compose } from `react-komposer`;
// Create a component to display Time
const Time = ({time}) => (<div>{time}</div>);
// Create the composer function and tell how to fetch data
const composerFunction = (props, onData) => {
const handler = setInterval(() => {
const time = new Date().toString();
onData(null, {time});
}, 1000);
const cleanup = () => clearInterval(handler);
return cleanup;
};
// Compose the container
const Clock = compose(composerFunction)(Time);
// Render the container
ReactDOM.render(<Clock />, document.getElementById('react-root')); Here's the live version: https://jsfiddle.net/arunoda/jxse2yw8/ We've also has some easy ways to compose containers with Promises, Rx.js Observables and With Meteor's Tracker. Also check my article on this: Let’s Compose Some React Containers |
@arunoda We wound up doing something very similar. One thing I'm wondering is how do you prevent composerFunction from being called on every prop change? |
@oztune Actually now it'll run again. We use this Lokka and Meteor. Both of those have local caches and does not hit the server even when we call the composerFunction multiple times. But I think we could do something like this: const options = {propsToWatch: ["postId"]};
const Clock = compose(composerFunction, options)(Time); Any ideas? |
@arunoda That's what we tried too, but it creates a bit of a disconnect between the action and its dependencies. We now do something similar to react-async, where, instead of immediately performing the given action, composerFunction would return a function and a key. If the key is different from the previous key returned by composerFunction, the new function will be performed. I'm not sure if this is a tangent from this github thread, so I'd be glad to continue on Twitter (same username). |
@oztune I created a new GH issue and let's continue our chat there. Much better than twitter I guess. |
Why not just let JSX to understand and render Observables directly so that I can pass Observable in props, e.g. this.props.todo$ and embed them in JSX. Then I wouldn't need any API, the rest is managed outside of React and HoC is used to fill in observables. It shouldn't matter if props contain plain data or an observable hence no special this.data is needed. {this.props.todo$
Additionally React render could be able to render Oservable[JSX] which would allow design described in the links without additional library. https://github.com/milankinen/react-combinators |
Hello, |
@giltig: I'm learning this way lately as well and like it. Together with Cycle.js. I would appreciate to be able to listen to event handlers defined on components somehow easily without having to pass in Subjects for bridging. If I understand correctly it is pretty much the other way around than what is suggested here. Alternatively, if we could observe React vdom for its synthetic events, maybe "refs" could be used for observing to avoid having CSS tags just to observe. Further RxJs could support object in combineLatest so that we could use React functional components directly to create larger components.
|
Hi, It would have made thinks easier if react components could accept pojos as long as observables as props. As for now it's not possible so what I've shown above is the way we chose. BTW what you did with MyFancyReactComponent is possible and we actually did that too in some cases though you can also write the jsx straightforward. Regarding subjects - I think it's a valid way because eventually in React component I'm just using a handler function which could be anything. I choose to implement it with subject inside that receives events but someone else can choose otherwise - its flexible. |
observable props have a no sense in the long run. It has no sense at all in that context, actually.. |
It sounds to me like we ended up with a different model with Suspense (cache + context). The cache itself might get support for subscriptions. You can track remaining work for Suspense in #13206. We also provide a subscription package for more isolated cases. |
This is a first-class API for sideways data loading of stateless (although potentially memoized) data from a global store/network/resource, potentially using props/state as input.
observe() executes after componentWillMount/componentWillUpdate but before render.
For each key/value in the record. Subscribe to the Observable in the value.
We allow onNext to be synchronously invoked from subscribe. If it is, we set:
Otherwise we leave it as undefined for the initial render. (Maybe we set it to null?)
Then render proceeds as usual.
Every time onNext gets invoked, we schedule a new "this.data[key]" which effectively triggers a forcedUpdate on this component. If this is the only change, then observe is not reexecuted (componentWillUpdate -> render -> componentDidUpdate).
If props / state changed (i.e. an update from recieveProps or setState), then observe() is reexecuted (during reconciliation).
At this point we loop over the new record, and subscribe to all the new Observables.
After that, unsubscribe to the previous Observables.
This ordering is important since it allows the provider of data to do reference counting of their cache. I.e. I can cache data for as long as nobody listens to it. If I unsubscribed immediately, then the reference count would go down to zero before I subscribe to the same data again.
When a component is unmounted, we automatically unsubscribe from all the active subscriptions.
If the new subscription didn't immediately call onNext, then we will keep using the previous value.
So if my
this.props.url
from my example changes, and I'm subscribing to a new URL, myContent will keep showing the content of the previous url until the next url has fully loaded.This has the same semantics as the
<img />
tag. We've seen that, while this can be confusing and lead to inconsistencies it is a fairly sane default, and it is easier to make it show a spinner than it would be to have the opposite default.Best practice might be to immediately send a "null" value if you don't have the data cached. Another alternative is for an Observable to provide both the URL (or ID) and the content in the result.
We should use the RxJS contract of Observable since that is more in common use and allows synchronous execution, but once @jhusain's proposal is in more common use, we'll switch to that contract instead.
We can add more life-cycle hooks that respond to these events if necessary.
Note: This concept allows sideways data to behave like "behaviors" - just like props. This means that we don't have to overload the notion state for these things. It allows for optimizations such as throwing away the data only to resubscribe later. It is restorable.
The text was updated successfully, but these errors were encountered: