universal-router icon indicating copy to clipboard operation
universal-router copied to clipboard

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

Open PaulMaly opened this issue 8 years ago • 8 comments

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).

PaulMaly avatar Dec 04 '16 14:12 PaulMaly

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'));
}

koistya avatar Dec 04 '16 17:12 koistya

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

PaulMaly avatar Dec 04 '16 18:12 PaulMaly

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....

bsunderhus avatar Aug 09 '17 22:08 bsunderhus

@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/

frenzzy avatar Aug 10 '17 08:08 frenzzy

@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

bsunderhus avatar Aug 11 '17 03:08 bsunderhus

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).

frenzzy avatar Aug 11 '17 12:08 frenzzy

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 avatar Aug 11 '17 16:08 bsunderhus

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

bsunderhus avatar Aug 11 '17 19:08 bsunderhus