Skip to content
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

I don't understand how "Universal Router" is working #72

Open
PaulMaly opened this issue Dec 4, 2016 · 8 comments · May be fixed by #86
Open

I don't understand how "Universal Router" is working #72

PaulMaly opened this issue Dec 4, 2016 · 8 comments · May be fixed by #86

Comments

@PaulMaly
Copy link

PaulMaly commented Dec 4, 2016

Hello, guys!

Your router looks great on a slides and simple in docs. Actually, too simple.

It promos like 'isomorphic', but I can't find how to work with HTML5 History API and NodeJS http requests too. It seems this router can't capture nothing and I need to call function "resolve" manually each time the route is changed (on the server and on the client in different ways). So, could you explain which part of this router is "isomorphic" and why it's called "router", if it can't observe routing paths by itself?

Thanks!

--- Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/39745948-i-don-t-understand-how-universal-router-is-working?utm_campaign=plugin&utm_content=tracker%2F18115217&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F18115217&utm_medium=issues&utm_source=github).
@koistya
Copy link
Member

koistya commented Dec 4, 2016

It doesn't handle the navigation part just routing. In order to watch for changes in URL you may want to use history npm module. Example:

// history.js
import createBrowserHistory from 'history/createBrowserHistory';
export default createBrowserHistory();

// main.js
import history from './history';
function render(location) { ... }
history.listen(location => {
  render(location);
});
render(history.location);

// Links
import history from './history';

function onClick(event) {
  event.preventDefault();
  history.push(event.currentTarget.attr('href'));
}

@PaulMaly
Copy link
Author

PaulMaly commented Dec 4, 2016

Thanks, @koistya, but without this part "router" unuseful for me. And I don't know why you call this library a "router".

@frenzzy frenzzy linked a pull request Mar 26, 2017 that will close this issue
20 tasks
@bsunderhus
Copy link

It's by decoupling from the history API that the universal-router is able to be called universal afterall @PaulMaly . Otherwise you wouldn't be able to use it in another non browser environment such as NodeJS Http server.
It's quite easy as demonstrated by @koistya to connect the universal-router to the History and Location API.

Still, I got a question @koistya about how this example that you gave above works together with React. Is it really a good approach to call render(history.location); in the root of the application??? I mean... it doesn't sound so performatic. You're reloading the whole stuff!!
Perhaps a more well located approach with a hoc maybe....

@frenzzy
Copy link
Member

frenzzy commented Aug 10, 2017

@BernardoS,
UniversalRouter allows to use routing code in different environments but entry points are usually platform specific anyway. See Isomorphic (universal) JavaScript concepts.

History module is not required. You may use the native History API if you want, it's up to you.

Yes, it is normal to re-render the app on location change. React takes care about performant updates. See Design Principles of React.js.

One more example:

// router.js - isomorphic (universal) code
import UniversalRouter from 'universal-router'
import React from 'react'

export default new UniversalRouter([ // all your routes here
  { path: '/', action: () => <h1>Home</h1> },
  { path: '/users', action: () => <h1>Users</h1> },
  { path: '/user/:id', action: (ctx) => <h1>User #{ctx.params.id}</h1> }
])
// client.js - entry point for browser
import ReactDOM from 'react-dom'
import router from './router.js'

const container = document.getElementById('app')
async function render() {
  const page = await router.resolve(location.pathname)
  ReactDOM.render(page, container)
}

render() // run client-side application

// in case if you need single page application
window.addEventListener('click', event => {
  if (event.target.tagName === 'A') {
    event.preventDefault()
    const anchor = event.target
    const state = null
    const title = anchor.textContent
    const url = anchor.pathname + anchor.search + anchor.hash
    history.pushState(state, title, url)
    render()
  }
})
// server.js - entry point for node.js server
import http from 'http'
import ReactDOMServer from 'react-dom/server'

const server = http.createServer(async (req, res) => {
  const page = await router.resolve(req.url)
  const html = ReactDOMServer.renderToString(page)
  res.end(`<!doctype html><div id="app">${html}</div>`);
});

server.listen(3000); // run server-side application

Demo: https://jsfiddle.net/frenzzy/fr1q4gne/

@bsunderhus
Copy link

bsunderhus commented Aug 11, 2017

@frenzzy I don't see where in the Design Principles of React.js you're seeing that is a good practice to do that.

Thanks for the other example but still, It seems that rendering the whole tree is a thing to be avoided in a React application. It apperently has the same performance as changing the state of the root component in the tree, and as you can see in multiple places and frameworks, such as Redux and Flux, the idea is to re-render the closer to the leaves in the tree as possible, that's why the normal practice in a Redux environment is to call the connect hoc to those leaves.

I'd love to see a big complex example of this re-rendering the root of the application idea in practice to see if i'm not going to suffer performance issues

@frenzzy
Copy link
Member

frenzzy commented Aug 11, 2017

The top-level element of any multi-page web application is the page. The only way to change a page is to replace it with a new one, which usually means changing the whole tree. The only case the whole tree update is not desirable when both pages contain the same elements. React cares about it under the hood by skipping DOM updates (React Only Updates What's Necessary). But in memory (in virtual-DOM) react will re-render the whole tree by default and there are some optimization techniques (Avoid Reconciliation).

@bsunderhus
Copy link

Still. Even if (React Only Updates What's Necessary) it also calls shouldComponentUpdate on every re-render cycle, even if the response is equall. In other words, even if React takes care of not reloading unecessary things, it doesn't mean that React is not doing a hard proccess on the background. Re-calling the render method on the root of the tree is a way more complex rendering lifecycle than calling in a leaf in the tree.

Saying with an example. If I had three routes like /, /items and /items/:id, where basically one route increments the other in the React virtual-dom tree: <Root><Items><Item id={params.id}/></Items></Root>.

If I were only in / then only <Root/> will appear. If I were in /items than <Root><Items></Items></Root>. The same way, if I were in the /items/123 the whole tree would appear.

You're basically suggesting that it's a good approach to call the whole process of validation of the whole tree to navigate through this routes. But this is an incremental case, where there's no reason to re-render <Root/> or <Items/> every time I change the :id parameters in the route, you got me now?

I got you're saying is no big deal because it's not gonna cause a re-render in the DOM itself, but it will re-render the whole virtual-dom tree. Therefore, doesn't seem like a good approach if you have a big application, this re-render could be too costy.

I know most of the routers do this, and that's the normal defacto way, but the new version of React Router has a little bit more elegant approach, where it only re-renders what has really changed.

@bsunderhus
Copy link

My mistake @frenzzy . I couldn't be more wrong when I said that

the new version of React Router has a little bit more elegant approach, where it only re-renders what has really changed.

I've got a little puzzled with React Router V4 and decided to do the exact test that I proposed abose

import React, {Component} from 'react'
import {Route, Link} from 'react-router-dom'
export default class App extends Component {
  render () {
    return (
      <div>
         <ul>
          <li><Link to="/">Root</Link></li>
          <li><Link to="/items">Items</Link></li>
          <li><Link to="/items/123">Item 123</Link></li>
          <li><Link to="/items/124">Item 124</Link></li>
        </ul>
        <Route path="/" component={Root}/>
      </div>
    )
  }
}
class Root extends Component {
  render () {
    console.log('render Root')
    return <div>
      this is Root
      <Route path="/items" component={Items}/>
    </div>
  }
}
class Items extends Component {
  render () {
    console.log('render Items')
    return <div>
      this is Items
      <Route path="/items/:id" component={Item}/>
    </div>
  }
}
class Item extends Component {
  render () {
    console.log(`render Item with id ${this.props.match.params.id}`)
    return <div>
      this is Item {this.props.match.params.id}
    </div>
  }
}

Ends up that the react-router v4 does the same proccess o re-rendering all the components that are connected, no matter if the changes are only in the Item Component

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants