Isomorphic usage of react-router #1990

Closed
mikhail-riabokon opened this Issue Sep 17, 2015 · 27 comments

Projects

None yet

7 participants

@mikhail-riabokon

Previously, I could use Router.run() for server and for client, the question is, can I use match() in this way?

For example

let params = {
 routes: routes,
 location: location
};

if (history) {
 params.history = history;
}

match(params, (/*args*/) => {
 // do something
});
@mikhail-riabokon

This example is just for server rendering, but I guess that match() can be used and on client, because I can pass history in params and probably it can be one of the history instances (Browser, Memory, Hash).

@BerkeleyTrue
Contributor

on the client:

const history = createHistory();

// It is not possible currently to get the location object synchronously through the history object.
// so we end up having two sources of truth for the current location or nesting the rest of this in a callback. 

const location = createLocation(location.pathname + location.search);
match({ location, history, routes }, (err, nextLocation, props) => {
  // props already has a history object
  // unfortunately this history object is wrapped by match, and Router will attempt to wrap it again
  // which will give you headaches. So here we override that history object with the first.
  // not a clean solution but it is the only solution at the moment.
  props.history = history;
  const inst = React.render(
      React.createElement(Router, props),
      DOMContianer
  );
});

Its been commented here and here that match was never intended to be used client side (and paradoxically that it was never intended to be public api), which means, by intent, if you are not using match client side you will end up writing code using different api's depending on the environment (server vs client) which is not really isomorphic friendly IMHO.

@mikhail-riabokon

Agree with you. In such case we will probably have something like this.

// client
let props = {
  history,
  location,
  children: routes
};

React.render(<Router {...props}/>, document.body);

//server
match({location, props}, (err, redirectInfo, renderedProps) => {
 // send response
});
@mikhail-riabokon

I still have problems, even this history replacements. Then routes changed on client, nothing happens.

@BerkeleyTrue
Contributor
// client
let props = {
  history,
  location,
  children: routes
};

React.render(<Router {...props}/>, document.body);

//server
match({ location, routes }, (err, redirectInfo, renderedProps) => {
 // send response
});
@mikhail-riabokon

So, to be clear, I can not use match() on client and on server even if I replace history?

@BerkeleyTrue
Contributor

Yes you can. I gave an example above on how to accomplish this. Are you seeing errors when using match?

@mikhail-riabokon

No, without errors, but and nothing happens. Routing does not work after initialization in this way.

@jmrog
jmrog commented Sep 19, 2015

@BerkeleyTrue, thanks for sharing your solution here. I thought that I was losing my mind(!) earlier today while trying to cook up an isomorphic app using the latest react-router. The client-side code was not receiving the right history information until I added the code you presented in this comment. I'm really hoping there's a better solution to this, but I'm not yet up-to-speed enough on all of the changes for v1.0.0 to suggest one.

@mikhail-riabokon

@BerkeleyTrue thank you, your solutions works.
An example of usage.

// client
import router from 'path/to/isomorphic/router';
import createLocation from 'history/lib/createLocation';
import createBrowserHistory from 'history/lib/createBrowserHistory';

const location = createLocation(document.location.pathname, document.location.search);
const history = createBrowserHistory();

router(location, history)
  .then((reactEl) => {
    React.render(reactEl, dist);
  }, (err) => {
    throw err;
  })

// server
import routing from 'path/to/routing/middleware';
// ...
app.use(routing)
// ...

// routing middleware
import React from 'react';
import createLocation from 'history/lib/createLocation'
import createMemoryHistory from 'history/lib/createMemoryHistory';
import router from 'path/to/isomorphic/router';

export default function(req, res) {
  let location = createLocation(req.url);

  router(location, history, res, req)
    .then((reactEl) => {
      try {
        let reactStr = React.renderToString(reactEl);
        res.send(reactStr);
      } catch (err) {
        res.status(500).send({error: err.toString()});
      }
    }, (err) => {
      console.log('err', err);

      res.status(500).send({error: err});
    });
}


// router
import Promise from 'promise';
import Router, {match, RoutingContext} from 'react-router';
import routes from './routes.js';

function getRootComponent(renderProps) {
  let component = null;

  if (__SERVER__) {
    component = (<RoutingContext {...renderedProps}/>);
  } else {
    component = React.createElement(Router, renderedProps);
  }

  return component;
}

export default function (location, history, res, req) {
  return new Promise((fullfill, reject) => {
    match({routes, location, history}, (error, redirectLocation, renderProps) => {
      if (!error) {
        if (renderProps) {
          renderProps.history = history;
        }  

        fullfill(getRootComponent(renderProps));        
      } else {
        reject(error);
      }
    });
  });
}
@mikhail-riabokon

I think this issue can be closed for v1.0.0-rc1.

@webular
webular commented Sep 22, 2015

@mikhail-riabokon In your example code how would you pass in props to your root component on the server?

@mikhail-riabokon

If I understood your question correct, the answer is

function getRootComponent(renderProps) {
  let component = null;

  if (__SERVER__) {
    component = (<RoutingContext {...renderedProps}/>);
  } else {
    component = React.createElement(Router, renderedProps);
  }

  return component;
}

where renderedProps props which passed to root component.

If not, could you explain which props do you want to pass?

@webular
webular commented Sep 22, 2015

Thanks for responding so quickly.

I have two objects website and page that my root component App will require in order to render properly. How you would recommend passing those two props to App. I hope this is clearer.

@webular
webular commented Sep 22, 2015

maybe this will help. I keep getting this:

Warning: Failed propType: Required prop website was not specified in Page. Check the render method of RoutingContext.

Can't figure out how to pass in the prop when rendering on the server...

@mikhail-riabokon

I understand that you mean.
For example you have an express app

...
const app = express();
...
app.use(express.static('path/to/assets'))
app.use(routing()) // isomorphic router
...

Then you go by route for example /, isomorphic router will receive renderedProps, but if you have some assets on the page, for example /asssets/images/home.png isomorphic router will not receive renderedProps, that's why you have such type of warnings

@mikhail-riabokon

I have updated an example of isomorphic usage redux-forms, where you can reach simple solution for your question.
https://github.com/mikhail-riabokon/redux-form-universal-example

@webular
webular commented Sep 22, 2015

Thanks - checking it out now.

@mikhail-riabokon

You're welcome. Let me know in case of any questions.

@nodu
nodu commented Oct 11, 2015

@webular Did you find a solution? I'm running into the same issue and can't find the solution in @mikhail-riabokon 's repo. TIA!

Is there a way to fix this issue by modifying RR Client side example here

<Router history={createBrowserHistory()} routes={routes} />

I've tried the above solution by @BerkeleyTrue , but I still get a server - client markup mismatch error and

Required prop `gifts` was not specified in `Gifts`. Check the render method of `RoutingContext`.
@mikhail-riabokon

@nodu Do you use for server rendering <RoutingContext />?

@nodu
nodu commented Oct 12, 2015

Yes @mikhail-riabokon I'm using it like this with Redux and Koa:

match({ routes, location: this.url }, (error, redirectLocation, renderProps) => {
  if (error) {
    this.body = error.message
  } else if (redirectLocation) {
    this.body = "Would Redirect to " + redirectLocation.pathname
  } else if (renderProps) {
    html = ReactDOMServer.renderToString(
      <div>
        <Provider store={store}>
          <RoutingContext {...renderProps}/>
        </Provider>
      </div>)
    this.body = renderFullPage(html, initialState)
  } else {
    this.body = "Not Found"
  }
})

Is there another way?

@webular
webular commented Oct 12, 2015

My use case was that I needed to pass two objects into my RoutingContext -- website and page.

Here is what I ended up doing.

function createElement(Component, props){
   return <Component {...props} website={website} page={page} />
}

return (<RoutingContext createElement={createElement} {...renderProps} />);

In order to pass those props into your Routing Context you need to use createElement.
https://github.com/rackt/react-router/blob/master/docs/API.md#createelementcomponent-props

In my App component I was then able to reference website and page via props. i.e. this.props.website

@nodu
nodu commented Oct 13, 2015

Thanks @webular for the code snippet, it's now working. But I have SERIOUS misgivings from this approach. I'm using several nested paths like this so I can have a smart component (CardsPage) that passes down all of the appropriate props for the two different nested dumb components Cards and Card:

<Route path="cards" component={CardsPage} >
      <IndexRoute component={Cards} />
      <Route path="card" path="/cards/:cardId" component={Card} />
    </Route>

With your solution and it seems to be the solution proposed by RR, I will have to pass each and every prop that ANY of my dumb components (app wide) would want into the RoutingContext. This just seems really inefficient and creates more boilerplate to write (and forget you need to write). I also pass actions as props to my dumb components in my flux implementation (Redux), so I would have to pass those into the RoutingContext as well. Is there a better, more sane solution?

@webular
webular commented Oct 13, 2015

Will creating a parent route around all your routes not work? Then I would sort out which props get passed down in the parent component?

I've got a parent component App which passes props to my "smart components". The smart components then pass props to dumb components.

Will that not work in your case?

@taion
Contributor
taion commented Oct 13, 2015

You can pass props into children from parents by cloning them. Otherwise React context is pretty good for this sort of thing.

Otherwise this discussion seems to have no bearing on this closed issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment