Repository navigation
Implement Sideways Data Loading #3398
Description
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.
type RecordOfObservables = { [key:string]: Observable<mixed> };
class Foo {
observe(): RecordOfObservables {
return {
myContent: xhr(this.props.url)
};
}
render() {
var myContent : ?string = this.data.myContent;
return <div>{myContent}</div>;
}
}observe() executes after componentWillMount/componentWillUpdate but before render.
For each key/value in the record. Subscribe to the Observable in the value.
subscription = observable.subscribe({ onNext: handleNext });We allow onNext to be synchronously invoked from subscribe. If it is, we set:
this.data[key] = nextValue;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.
subscription.dispose();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.
class Foo {
observe() {
return {
user: loadUser(this.props.userID)
};
}
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>;
}
}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.
var subscription = observable.subscribe({ onNext, onError, onCompleted });
subscription.dispose();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.
Activity
josephsavona commentedon Mar 13, 2015
undefinedis probably the safest value to assign todatauntil the observable provides its first value viaonNext. For example in Relay we assign different meanings tonull(data does not exist) andundefined(not yet fetched), so our ideal default data value would beundefined. The alternative is to provide a new method, eggetInitialData, but I suspect this is unnecessary/overkill.fdecampredon commentedon Mar 13, 2015
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.
Why not having
observereturn a single observable and set/merge the value resolved tothis.data:And for the case of multiple fetch something like :
This is perhaps a bit more verbose, but it allows to have nice static type :
fdecampredon commentedon Mar 13, 2015
Also instead of re executing
observewhen props/state change we can have access to 'props' 'state' as an observable :sebmarkbage commentedon Mar 13, 2015
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.
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.
vjeux commentedon Mar 13, 2015
cc @ericvicenti
fdecampredon commentedon Mar 13, 2015
At least in the case of typescript, there would be no way to constraint the return type of
observebased on the type ofdata, at least until something like microsoft/TypeScript#1295 is implemented.gaearon commentedon Mar 13, 2015
I'd love to use this for the next version of React DnD, but obviously this requires waiting for React 0.14.
I wonder if I can “polyfill” this for the time being with a higher-order component that sets
this.dataon arefinstance.. Might be too crazy though.RickWong commentedon Mar 14, 2015
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.
aaronshaf commentedon Mar 14, 2015
What are the benefits of making this a first-class API? It could essentially be accomplished using a "higher-order component."
gaearon commentedon Mar 14, 2015
Wrapping in 5 HOCs to get 5 subscriptions is a bit unwieldy and harder to understand for beginners. Understanding
componentWillReceivePropsis also non-trivial. This fixes both.I, for one, welcome our new observable overlords.
gaearon commentedon Mar 14, 2015
I wonder if this can help bring https://github.com/chenglou/react-state-stream closer to React's vanilla API
aaronshaf commentedon Mar 14, 2015
Wouldn't it just take one HOC? In the example in your Medium post, you iterate over
storesand subscribe to each.gaearon commentedon Mar 14, 2015
@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.
nmn commentedon Mar 14, 2015
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.
elierotenberg commentedon Mar 14, 2015
(@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
getNexusBindingshook in React Nexus. You declare data deps at the component level via a lifecycle hook (which can depend on props).The API looks like:
The binding is applied/updated during
componentDidMountandcomponentWillReceiveProps. In the latter case, the next bindings are diffed with the previous bindings; removed bindings are unsubscribed, added bindings are subscribed. The underlying fetching/updated mechanism is described in the implementation of Nexus Flux. Basically with the same API you can either subscribe to local data (traditional local stores) or remote data (fetch using GET and receive patches via Websockets/polyfill). You could actually subscribe to data from another window (using postWindow) or a WebWorker/ServiceWorker but I still haven't found a truly useful use case for this.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.
109 remaining items
oztune commentedon Nov 16, 2015
@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.
jimfb commentedon Nov 16, 2015
@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.
arunoda commentedon Jan 3, 2016
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.
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
oztune commentedon Jan 3, 2016
@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?
arunoda commentedon Jan 3, 2016
@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:
Any ideas?
oztune commentedon Jan 3, 2016
@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).
arunoda commentedon Jan 4, 2016
@oztune I created a new GH issue and let's continue our chat there. Much better than twitter I guess.
vladap commentedon Jan 17, 2016
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.
Additionally React render could be able to render Oservable[JSX] which would allow design described in the links without additional library.
https://medium.com/@milankinen/containers-are-dead-long-live-observable-combinators-2cb0c1f06c96#.yxns1dqin
https://github.com/milankinen/react-combinators
giltig commentedon Mar 1, 2016
Hello,
As for now we could use stateless components with rxjs streams easily.
I dont understand the need for another API.
I wrote an example - a board that you can move mouse over it and when it gets to 26 it changes to restart.
I would love to hear what you think about this way.
Here is the code:
https://jsfiddle.net/a6ehwonv/74/
vladap commentedon Mar 2, 2016
@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.
giltig commentedon Mar 2, 2016
Hi,
The reason we don't use Cycle or any other framework is that I prefer libraries over frameworks (more control to the developer) and also I want to enjoy the community power that React has.
So many render engines are now developed for React and it's a pity not to use it (ReactNative, ReactDom, ReactThree etc..). It's pretty simple to use just Rxjs and react as the example I've shown above.
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.
lexfrl commentedon Nov 20, 2016
observable props have a no sense in the long run. It has no sense at all in that context, actually..
gaearon commentedon Aug 15, 2018
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.