Skip to content

Routes rendered by a component? #262

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

Closed
olance opened this issue Sep 4, 2014 · 49 comments
Closed

Routes rendered by a component? #262

olance opened this issue Sep 4, 2014 · 49 comments

Comments

@olance
Copy link

olance commented Sep 4, 2014

I've literally spent hours on that and I've come to believe what I want is not (yet?) possible.

I probably have a bit of an uncommon setup: I am using React within a Rails application using react-rails and browserify-rails to get things working together.

Before trying to use react-router, I had the following setup (a sample/simple blog app):

<App>
  |__<div>
         |__ <h1>
         |__ <PostsList>

The App component would initialize an app context, register a PostStore and dispatch an initial_state message to fill the store with posts fetched on the server when rendering the page.
Using react-rails, all of this would render on the server and then be picked up by the React UJS script, simply by calling the react_component('App', {initialState: @app_state}) view helper in my Rails layout.

Now, I want to display a <PostShow> component instead of my <PostsList> when the user clicks on a post title. I chose to try react-router to handle the route changes and UI updates implied after talking to @mjackson on IRC ^^

Because I am using react-rails, what I'd like to do is keep this unique entry point through my App component and the react_component call.
So I have renamed App to AppRouter and changed the render code of this main component to draw my routes instead:

render: function () {
        return (
            <Route path={RouteHelpers.root_path()} handler={App}>
                <DefaultRoute handler={PostsIndex} name="root"/>
                <Route path={RouteHelpers.post_path.spec()} handler={PostShow} name="post" />
            </Route>
        );
    }

(I originally had a parent <Routes location="history"> but have removed it after reading #40... not sure that was the right thing to do?)

The new App component uses activeRouteHandler to display <PostsIndex> or <PostShow> depending on the active route.

I really, really thought the would work out nicely, but I keep getting this error:
AppRouter.render(): A valid ReactComponent must be returned. You may have returned undefined, an array or some other invalid object

Apparently, <Route(s)> isn't a React component. Is it not??!
The codebase is quite large but I did find that Routes = React.createClass(...) so why would React tell me it's not a ReactComponent?

What have I missed? Is there a way to achieve what I've described here?

@ryanflorence
Copy link
Member

<Routes/> is intended to be used as the top-most component, not inside the render of another.

We're really close on server-side rendering and then maybe there will be a better way to integrate with react-rails, but for now you're going to have to figure something else out :(

@ryanflorence
Copy link
Member

Oh, forgot to answer the question.

<Route/> does not render anything. It is simply a config component that <Routes/> uses.

https://github.com/rackt/react-router/blob/master/modules/components/Route.js#L91-L96

@olance
Copy link
Author

olance commented Sep 4, 2014

Funny I don't even get the error from Route render function!

Thanks for the confirmation, I'll try to figure out something else then :\

@olance
Copy link
Author

olance commented Sep 4, 2014

Can you maybe explain why <Routes> cannot be used as a child component?
As I see it, Routes will render null or the handler prop for the matched Route, and I can't figure out why this couldn't be done inside another top-level component?

If you have the time, I'd really love to understand the difference with a regular component... thanks!

It's just out of pure curiosity and will to understand what's going on under the hood. I have figured out that since there's no server-side rendering at all, I can reasonably include a <script> tag that calls renderComponent with my routes for the moment :)

@ryanflorence
Copy link
Member

I'm not sure why exactly it doesn't work, but the design of this router is to be the thing that kicks off your app, not to be embedded in another app.

@olance
Copy link
Author

olance commented Sep 4, 2014

Yep I understood that, but what's the real difference between calling React.renderComponent with a <Route> set, and rendering that same route set from a component that is itself being rendered using React.renderComponent?

The fact that it doesn't work is quite counter-intuitive for me :|

@mjackson
Copy link
Member

mjackson commented Sep 4, 2014

@olance <Route> components are never actually rendered. They're only used to configure the router. So the only component that knows what to do with them is <Routes>.

It's a little odd, I grant you. But the reason we used it is because the XML-like syntax of JSX makes it really easy to quickly scan the nesting of your routes and their relationship to one another.

@olance
Copy link
Author

olance commented Sep 4, 2014

yeah but what about <Routes>? :)

@mjackson
Copy link
Member

mjackson commented Sep 4, 2014

@olance Hmm ... I actually think that should work. Off the top of my head I can't think of any reason it shouldn't... :)

@mjackson mjackson reopened this Sep 4, 2014
@mjackson
Copy link
Member

mjackson commented Sep 4, 2014

Re-opening since I think @olance has a good point

@olance
Copy link
Author

olance commented Sep 4, 2014

Ah! Well... it doesn't :D
Or I have done something wrong, but couldn't figure out what the whole night I spent on it ^^

@olance
Copy link
Author

olance commented Sep 4, 2014

thanks! 👍

@mjackson
Copy link
Member

mjackson commented Sep 4, 2014

:/ Sorry you spent a whole night on it. As @rpflorence mentioned, we never intended <Routes> to be used in this way, so we haven't ever tested it.

@olance
Copy link
Author

olance commented Sep 4, 2014

Ah, no worries! It's not your fault, I just wanted it my way ;)
However if you think it could/should work, I am very interested!

@mjackson
Copy link
Member

mjackson commented Sep 4, 2014

Absolutely. I still need to think about it a little more and discover why it's failing in the first place, but as I said I don't see any reason right now why it shouldn't work. I'll look into it :)

@olance
Copy link
Author

olance commented Sep 4, 2014

Thanks a bunch :)
The only thing I gathered from my debug traces is that somewhere in the rendering process, renderedComponent has a descriptor that is not an instance of a React descriptor, it's simply an Object instance.

Not sure it'll help you though ^^

@ryanflorence ryanflorence added this to the props Props PROPS milestone Oct 11, 2014
mjackson added a commit that referenced this issue Oct 13, 2014
This commit adds the ability for route handlers to load props
they need for a given route, optionally asynchronously. Props
loaded in this manner cascade from route handlers further up
the hierarchy to children, such that props loaded by children
take precedence.

getRouteProps should return a "hash" of props, the values of
which may either be immediate or deferred using promises. As
promises resolve, forceUpdate is used to re-render the <Routes>.
If no props are promises, the operation is fully synchronous
and route handlers will have all the props they need on the
initial render.

As implemented, this work should satisfy the use cases in #319,
 #261, #314, #374, and (indirectly) #262.
@gauravtiwari
Copy link

Hi @olance Was you able to get the routes working with rails ? Thanks

@olance
Copy link
Author

olance commented Nov 5, 2014

@gauravtiwari nope. I have left the subject for later, as I had other things to work on that were a bit more urgent for me. Now that the server-side rendering is ready, I guess this bug will come in focus again shortly...

@mjackson any news? :)

@gauravtiwari
Copy link

Thanks @olance. Yeah, server side rendering is working great from a while now. I actually integrated this small library called page.js for routing and it's working pretty well however, it will be good to have something that is built for react. Hopefully, it will be fixed soon.

Thanks for your reply.

@olance
Copy link
Author

olance commented Nov 5, 2014

hmm interesting, so are you using React-router on the server side and page.js on the client side for the moment, or are you waiting for react-router to have this bug fixed to use it?

@gauravtiwari
Copy link

@olance React router is not working at all. I encountered many bugs while using it (not sure, it may be just me). So basically, I used page.js to render components by passing params or objects as a props to that component. One can do like -

componentDidMount: function () {
var self = this;
page('/posts/:id', function (data) {
      self.setState({ component: <PostShow data = {data Or whatever prop or state} /> });
 });
page.start();
},

render:function() {
return (
  {this.state.component} //this changes with data
)
}

You can then have a list of post on a master component and just navigate around by passing data(props or states).

I think one can achieve this together with rails on having the full-page rendered using server side on page reload and use page.js to navigate around sub-pages with client side routing and react.

BTW, this one also looks interesting. I am experimenting with it - React router component

@ryanflorence
Copy link
Member

in v0.11 this should work fine now.

@ryanflorence
Copy link
Member

just realized we need a teardown (which I have a branch for already) for this to really work, will push soon, probably today.

@olance
Copy link
Author

olance commented Dec 1, 2014

hey @rpflorence :)
I hadn't noticed you had closed the issue... so what's the status now? ^^

@ryanflorence ryanflorence reopened this Dec 1, 2014
@ryanflorence
Copy link
Member

I have the code for this, need to push as soon as I'm done with this other thing I'm doing and then there's this yak also that I'm shaving ...

@olance
Copy link
Author

olance commented Dec 1, 2014

Ahah okay, good luck with the yak then! ;)

🐃

@gauravtiwari
Copy link

thanks @rpflorence

@rohit8
Copy link

rohit8 commented Dec 4, 2014

+1

@eikeon
Copy link

eikeon commented Dec 5, 2014

@rpflorence any chance you could push your code to a branch while you're shaving that yak? Seems to be a few of us that might give it a look in the meantime.

@andrew-d
Copy link

andrew-d commented Dec 6, 2014

@rpflorence I'm running into something similar when trying to use react-router and Morearty. To see the problem in code, see here, or more discussion on the Morearty project here. Any idea if your solution will allow this?

@krashidov
Copy link

@olance I read part 1 of your blog post[1] about the server side rendering, and I was wondering why you never posted a part 2. Now, I see you've been stuck on the same thing I've been stuck with :) . I was wondering if you ever got this to work.

Thanks!
[1] The blog post I mentioned - https://medium.com/@olance/isomorphic-reactjs-app-with-ruby-on-rails-part-1-server-side-rendering-8438bbb1ea1c

@olance
Copy link
Author

olance commented Dec 23, 2014

@krashidov yeah I've been stuck on this indeed, plus I haven't had so much free time to investigate and write the second part! :(
It will definitely be coming though!

@rpflorence I'd +1 @eikeon on this, maybe we could have access to your working branch for this and help a bit?

@gaearon
Copy link
Contributor

gaearon commented Dec 23, 2014

@olance @krashidov

What is your problem exactly? Current version of router lets you render whenever you want (code taken from this issue):

// This is a reference to my Morearty binding object.
var binding = ...;

// Keep a reference to the "current handler", which is the React component
// class that is at the root of your route hierarchy.
var CurrentHandler;

function renderTheCurrentHandler() {
  React.render(<CurrentHandler binding={binding}/>, document.body);
}

binding.addListener(function () {
  // This is my notification that something in the binding changed.
  renderTheCurrentHandler();
});

Router.run(routes, function (Handler) {
  // This is my notification that the route changed.
  CurrentHandler = Handler;
  renderTheCurrentHandler();
});

@olance
Copy link
Author

olance commented Dec 30, 2014

@gaearon my problem is described in the first post for this issue ;)
The TL;DR is: I'd like my router not to be the root component of my React components hierarchy.

Apparently @rpflorence still has some work to do on the codebase to make it all work. However, I have not tested again since the last numerous improvements to the component.

@gaearon
Copy link
Contributor

gaearon commented Dec 30, 2014

@olance That's why I was asking: you can do this now since 0.11.x. For example, see #547 (comment).

However I still don't understand why you want to do that. It's way easier to make App top-level route and use router's nesting feature. Have you read about it? https://github.com/rackt/react-router/blob/master/docs/guides/overview.md#with-react-router

@olance
Copy link
Author

olance commented Jan 5, 2015

Thanks for the link @gaearon, it should help me out :)

I have read about it, but what I'm trying to achieve would be much nicer with a simple wrapper.

Because I am using react-rails, what I'd like to do is keep this unique entry point through my App component and the react_component call.

Using react-rails implies using the react_component(componentName, stateObject) view helper. This helper outputs some markup that'll be picked up by React Unobtrusive JS script to render whatever is needed to be rendered. componentName here is a string, I cannot (and wouldn't) put JSX/virtual DOM there to have my routes at the top level.
That's why I'd rather keep my App/AppRouter top-level object that could then render the router, depending on other stuff like declared routes on my Rails app and so on.

I'll try to get back at testing all this during the week and see what happens! ^^

@ryanflorence
Copy link
Member

With 0.11.x you can do it like this now: http://jsbin.com/duyaxa/1/edit?html,js,output

@gauravtiwari
Copy link

thanks @rpflorence! Will this work server side as well together with react-rails gem ?

@ryanflorence
Copy link
Member

I don't know why it wouldn't, but I don't know anything about the react-rails gem.

@attekei
Copy link

attekei commented Mar 1, 2015

Works well with react-rails gem! :)

If you use something like react_component('App', {initialState: @app_state}), then you can delegate the state to other components like this:

return Handler ? <Handler initialState={this.props.initialState} /> : null;

And then deeper in the route hierarchy in similar fashion:

<RouteHandler initialState={this.props.initialState} />

@GabKlein
Copy link

The problem with the top level App is you cannot prerender specific component.
Instead I would like to use my element directly like before react_component('HomePage', {initialState: @app_state}, { prerender: true }) and react_component('OtherPage', {initialState: @app_state}, { prerender: true }) for each views.

Prerender is very useful feature for SEO. It would be nice to be able to kick off the router when Javascript is enable and default to the component itself otherwise.

When I try this approach I get context errors (makePath, makeHref, transitionTo, replaceWith, etc)

@Attrck Could you explain a bit more how you implemented it? A gist would be awesome!

@stefanRitter
Copy link

@GabKlein @Attrck pre-rendering doesn't work, because context is lost. also react_ujs breaks it on the client.
I think we'll have to file an issue with react-rails - When testing it with @ryanflorence 's setup it works: http://jsbin.com/venofuqabi/1/edit?html,js,output

@stefanRitter
Copy link

Actually sorry it's not an issue with react-rails. The issue is that we want to render the app before we created a router. Like in this case where the initial state already has a handler: http://jsbin.com/mirajedadi/1/edit?html,js,console

@ryanflorence do you have an idea how to easily render the app before the router is run? Like a synchronous call to Router.run() that returns the HTML string of the set route.

@GabKlein
Copy link

The problem would be to generate the proper href attribute for Link. Without the router enabled, I don't how the mapping can take place.

@mpereira
Copy link

@ryanflorence: With 0.11.x you can do it like this now: http://jsbin.com/duyaxa/1/edit?html,js,output

I tried to build off your example but navigation doesn't seem to work. Clicking the Links result in no transition. Maybe I'm doing something wrong?

http://jsbin.com/tapelidezi/1/edit?html,js,console,output

@benbonnet
Copy link

@Attrck is that just me or the react-rails-router's README examples are absolutely unobvious ?

@attekei
Copy link

attekei commented Apr 9, 2015

@bbnnt I don't know react-rails very well because I only have done frontend development in our project.

We only pass the initial state to React app with react-rails. Code snippet from @rpflorence solved the problem with that. We haven't tried pre-rendering yet. Actually I didn't notice earlier that this conversation was about server-side rendering 😄

@ghost
Copy link

ghost commented May 7, 2015

This is how I have react-router working with react-rails. With react-rails helper, pre-render 'StaticRoute' component with an additional path prop. React-router will take care of rendering the right handler in execjs.

var StaticRoute= React.createClass({
  render: function(){
    var RouteHandler = Router.run(routes, this.props.path, function(){});
    return (
          <RouteHandler {...this.props} path={undefined} />
      );
  }
});

After reviewing the code I noticed Router.run returns the router instance with the current handler, which can be used to render the actual component, keeping the router context intact. The callback function in run is needed for location change setup. Which is not needed for server side rendering. Execjs does not support async anyway.

Even though SEO works well, the down side of this approach is, when react runs in the browser it gets confused because it is expecting 'Router' component, not the 'StaticRoute' component in dom. react-rails-ujs does the setup of this pre-rendered component. So before I run react-router in the browser, I remove all HTML from container, and let react do everything from scratch.

@lock lock bot locked as resolved and limited conversation to collaborators Jan 24, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests