-
Notifications
You must be signed in to change notification settings - Fork 52
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
Prepare for React v0.13+ #8
Conversation
Yes, this pattern is problematic. Even in 0.12, because a consumer of this component might expect this to remain a promise and you're changing it from under them. I would recommend that you implement this as a higher-order component. That way you have a layer where you are in full control over all the props without risk colliding with other mixins etc. https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775 It will also work for any kind of React component. Including ES6 classes. |
Well, it doesn't really change it out from underneath them. This enables the possibility that: Is the only way to really do this in the future will be "wrapper" components, since In which case, will this mean that every component that wants to lazy-load data should use a wrapper? (It may be worth confirming or denying if this is how Relay is injecting props, since we have the exact same method of solving this problem :D) P.S. Thanks for joining the discussion @sebmarkbage! |
That props object is the same object as the one in the parent's element. So this (somewhat contrived) pattern would break: var constantElement = <MyLazyComponent user={loadSomePromise()} />;
var Foo = React.createClass({
handleClick: function() {
constantElement.props.user.then(() => doSomething()); // Throws since this is not a promise.
},
render: function() {
return <div onClick={this.handleClick}>{constantElement}</div>;
}
}); This is just illustrative but we've had many related problems with this in the past. Relay tried to clone and replace the props object leading to a bunch of complexities and broken things. Relay just switched to using wrappers instead. Simplifies things quite a bit. Note that it is not true that every component that wants to lazy-load data should use a wrapper. You can use life-cycles to update your own state. That's fine. Every time you want to pretend that you're props are something different than was passed in, you have to use a wrapper. |
@sebmarkbage I see, so the take-away is:
To clarify, though, this project does not override any existing props. If for some (ridiculous) reason, you specify & use a You can test I just wanted to ensure you don't think I'm doing some stupid with this project. I certainly hope that Relay "fills in gaps in @sebmarkbage Thanks again for the feedback and confirmation that Relay uses a wrapper to solve this same problem. This means my own & others' projects won't be left in the lurch when React v0.13+ gets released! To those following, I'll begin a feature branch for v0.13 support to use wrappers instead of |
I better get started!
|
Lots to work with here:
|
@ericclemmons From looking at the way you have it implemented now, if you just switch to setting the values from the promise as state instead of props then you should be fine (and of course the render method will use state instead of props), you will no longer be mutating props. |
@jeffbski: Good point! That may be the middle-ground for v0.2.x to work with React v0.13, but I dare say I'll have to rewrite with |
@ericclemmons Relay works with the same on 0.12 and 0.13. We are not using context for data, only for the route and that's only on the containers. To the user, the route is passed in as a prop from the container to the inner component; the fact that behind the scenes containers use context for the route is transparent to the user. |
@cpojer I'd love to know a bit more about the architecture on a smaller scale. The last I heard was that contexts were used, but that can mean most anything, especially when now there's mention of a (1) route, (2) container, (3) inner component and (4) context :) If you're up for discussing more, I can be found via http://reactiflux.slack.com/, or eric@smarterspam.com. Also, this project was originally inspired by ui-router before Relay was released, but clearly serves similar goals! |
I see. On a small scale, Relay containers are very simple: class MyComponent extends React.Component {
render() { return <div />; }
}
var MyContainer = createContainer(MyComponent); This wraps 'MyComponent' (the 'inner component') in another component that we call a RelayContainer. Relay containers are responsible for getting the requested data into the inner component via props. We have a root component that injects the current route as It is pretty much exactly like @sebmarkbage's example in his first comment, except of course containers do all the heavy lifting and interfacing with the internals of Relay :) |
@cpojer: Thank you so much for this very clear example! |
So @cpojer your above example in more detail might look like the following? const MyComponentClassProps = { // applied to class
contextTypes: {
route: React.PropTypes.object.isRequired
}
};
class MyComponent extends React.Component {
render() { return <InnerComponent route={this.context.route} />; }
}
Object.assign(MyComponent, MyComponentClassProps);
var MyContainer = createContainer(MyComponent); Just trying to make sure I understand the above discussion. Thanks. |
After writing that, I realized that you are probably abstracting all the context stuff in the higher order wrapper so that data is simply coming in props. class MyComponent extends React.Component {
render() { return <InnerComponent route={this.props.route} />; }
}
var MyContainer = createContainer(MyComponent); That cleans things up if you handle all that in the container wrapper. |
@jeffbski no, you essentially implemented the container above (edit: saw your second comment, yes, you are correct :) ). Here let's take a look at this: // container implementation
function createContainer(InnerComponent) {
const ContainerStatics = { // applied to class
contextTypes: {
route: React.PropTypes.object.isRequired
}
};
class Container extends React.Component {
// do lots of magic in lifecycle methods
render() { return <InnerComponent route={this.context.route} />; }
}
Object.assign(Container, ContainerStatics);
return Container;
}
// usage:
class MyComponent extends React.Component {
render() { return <div>{this.props.route.name}</div>; }
}
MyComponent.propTypes = {
route: React.PropTypes.instanceOf(Route),
};
var MyContainer = createContainer(MyComponent); There is really nothing special about how containers work. Most mixins can be modeled as containers, it is really just wrapping one component with another, in this case the container is dynamic and kinda implicit (one container per createContainer call gets created) instead of explicit wrapping of your components in render + cloning which you are probably more used to. Consider this example: // one-of implementation
class MyComponent extends React.Component {
render() { return <div>{this.props.route.name}</div>; }
}
class MyComponentContainer extends React.Component {
render() { return React.cloneElement(React.Children.onlyChild(this.props.children), {
// props to inject to the inner component
}); }
}
// usage
class MyView extends React.Component {
render() {
return (
<MyComponentContainer>
<MyComponent />
</MyCompontentContainer>
);
}
} Now with this approach you'll have to use cloneElement but you also need to explicitly specify both the container and the inner component at every call site. The |
@ericclemmons we'll talk about Relay's architecture a bit more in some upcoming blog posts :-) |
@josephsavona Sounds good! This is a pattern that, in the meantime, helped move our data needs from Handlers/Controllers to Components, so development will continue. I just want to ensure that myself & others will have a smooth transition path should Relay be a better solution for those of us not using GraphQL (yet :D). Thanks! |
@ericclemmons of course! not suggesting otherwise, this is a helpful pattern 👍 |
@cpojer Thanks for the wonderful explanation. That helps tremendously. |
Alright, I got a functional version of this ready to test! I'll be committing momentarily... |
This is a requirement for server-side rendering, as any lazy-loaded <Container /> needs to store it's output for a synchronous final React.renderToString. Additionally, `resolver.freeze` ensures an error is thrown if any un-resolved Promises are triggered.
Whew boy! d748508 was a fun one to put in. I initially thought I'd only track resolved data via Plus, this is a requirement to ensure that the 1st render on the server loads all of the data, and the 2nd render uses the cached data for a synchronous Still, there's some more to do to ensure that deeply-nested containers track & resolve as expected... |
Sweet, so this is pretty much ready for release! A couple little nit-picks:
|
Forgot a couple things:
|
Alright, I'm going to merge this guy down, finish off some docs, then tag & release! Thanks for the help from everyone! Mucho <3! |
👏 |
Sebastian Markbåge wrote about Immutable Props here:
react-resolver
does a "double mount/render": oncecomponentWillMount
to initially fire off a promise for data dependencies, then for the second pass to fill in the gaps inprops
:This technique is used because
this.setProps
andthis.replaceProps
can only be down on root-level components.The question is, will this pose any issues dynamic prop resolution with 0.13?
/ping @sebmarkbage