Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon

Build a Universal JavaScript App, Part 2

Save for later
  • 10 min read
  • 30 Sep 2016

article-image

In this post series, we will walk through how to write a universal (or isomorphic) JavaScript app. Part 1 covered what a universal JavaScript application is, why it is such an exciting concept, and the first two steps for creating our app, which are serving post data and adding React. In this second part of the series, we walk through steps 3-6, which are client-side routing with React Router, server rendering, data flow refactoring, and data loading of the app. Let’s get started.

Save on some of our very best React and Angular product from the 7th to 13th November - it's a perfect opportunity to get stuck into two tools that are truly redefining modern web development. Save 50% on featured eBooks and 80% on featured video courses here.

Step 3: Client-side routing with React Router

git checkout client-side-routing && npm install

Now that we're pulling and displaying posts, let's add some navigation to individual pages for each post. To do this, we will turn our list of posts from step 2 (see the Part 1 post) into links that are always present on the page. Each post will live at http://localhost:3000/:postId/:postSlug. We can use React Router and a routes.js file to set up this structure:

// components/routes.js
import React from 'react'
import { Route } from 'react-router'
import App from './App'
import Post from './Post'

module.exports = (
<Route path="/" component={App}>
   <Route path="/:postId/:postName" component={Post} />
</Route>
)

We've changed the render method in App.js to render links to posts instead of just <li> tags:

// components/App.js
import React from 'react'
import { Link } from 'react-router'

const allPostsUrl = '/api/post'

class App extends React.Component {
constructor(props) {
   super(props)
   this.state = {
     posts: []
   }
}

...

render() {
   const posts = this.state.posts.map((post) => {
     const linkTo = `/${post.id}/${post.slug}`;

     return (
       <li key={post.id}>
         <Link to={linkTo}>{post.title}</Link>
       </li>
     )
   })

   return (
     <div>
       <h3>Posts</h3>
       <ul>
         {posts}
       </ul>

       {this.props.children}
     </div>
   )
}
}

export default App

And, we'll add a Post.js component to render each post's content:

// components/Post.js
import React from 'react'

class Post extends React.Component {
constructor(props) {
   super(props)

   this.state = {
     title: '',
     content: ''
   }
}

fetchPost(id) {
   const request = new XMLHttpRequest()
   request.open('GET', '/api/post/' + id, true)
   request.setRequestHeader('Content-type', 'application/json');

   request.onload = () => {
     if (request.status === 200) {
       const response = JSON.parse(request.response)
       this.setState({
         title: response.title,
         content: response.content
       });
     }
   }

   request.send();

}

componentDidMount() {
   this.fetchPost(this.props.params.postId)
}

componentWillReceiveProps(nextProps) {
   this.fetchPost(nextProps.params.postId)
}

render() {
   return (
     <div>
       <h3>{this.state.title}</h3>
       <p>{this.state.content}</p>
     </div>
   )
}
}

export default Post

The componentDidMount() and componentWillReceiveProps() methods are important because they let us know when we should fetch a post from the server. componentDidMount() will handle the first time the Post.js component is rendered, and then componentWillReceiveProps() will take over as React Router handles rerendering the component with different props.

Run npm build:client && node server.js again to build and run the app. You will now be able to go to http://localhost:3000 and navigate around to the different posts. However, if you try to refresh on a single post page, you will get something like Cannot GET /3/debugging-node-apps. That's because our Express server doesn't know how to handle that kind of route. React Router is handling it completely on the front end. Onward to server rendering!

Step 4: Server rendering

git checkout server-rendering && npm install

Okay, now we're finally getting to the good stuff. In this step, we'll use React Router to help our server take application requests and render the appropriate markup. To do that, we need to also build a server bundle like we build a client bundle, so that the server can understand JSX. Therefore, we've added the below webpack.server.config.js:

// webpack.server.config.js
var fs = require('fs')
var path = require('path')

module.exports = {

entry: path.resolve(__dirname, 'server.js'),

output: {
   filename: 'server.bundle.js'
},

target: 'node',

// keep node_module paths out of the bundle
externals: fs.readdirSync(path.resolve(__dirname, 'node_modules')).concat([
   'react-dom/server', 'react/addons',
]).reduce(function (ext, mod) {
   ext[mod] = 'commonjs ' + mod
   return ext
}, {}),

node: {
   __filename: true,
   __dirname: true
},

module: {
   loaders: [
     { test: /.js$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react' }
   ]
}
}

We've also added the following code to server.js:

// server.js  
import React from 'react'
import { renderToString } from 'react-dom/server'
import { match, RouterContext } from 'react-router'
import routes from './components/routes'

const app = express()

...

app.get('*', (req, res) => {
match({ routes: routes, location: req.url }, (err, redirect, props) => {
   if (err) {
     res.status(500).send(err.message)
   } else if (redirect) {
     res.redirect(redirect.pathname + redirect.search)
   } else if (props) {
     const appHtml = renderToString(<RouterContext {...props} />)
     res.send(renderPage(appHtml))
   } else {
      res.status(404).send('Not Found')
   }
})
})

function renderPage(appHtml) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Universal Blog</title>
</head>
<body>
   <div id="app">${appHtml}</div>
   <script src="/bundle.js"></script>
</body>
</html>
`
}

...

Using React Router's match function, the server can find the appropriate requested route, renderToString, and send the markup down the wire. Run npm start to build the client and server bundles and start the app. Fantastic right? We're not done yet. Even though the markup is being generated on the server, we're still fetching all the data client side. Go ahead and click through the posts with your dev tools open, and you'll see the requests. It would be far better to load the data while we're rendering the markup instead of having to request it separately on the client.

Since server rendering and universal apps are still bleeding-edge, there aren't really any established best practices for data loading. If you're using some kind of Flux implementation, there may be some specific guidance. But for this use case, we will simply grab all the posts and feed them through our app. In order to this, we first need to do some refactoring on our current architecture.

Step 5: Data Flow Refactor

git checkout data-flow-refactor && npm install

It's a little weird how each post page has to make a request to the server for its content, even though the App component already has all the posts in its state. A better solution would be to have an App simply pass the appropriate content down to the Post component.

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at $19.99/month. Cancel anytime
// components/routes.js
import React from 'react'
import { Route } from 'react-router'
import App from './App'
import Post from './Post'

module.exports = (
<Route path="/" component={App}>
   <Route path="/:postId/:postName" />
</Route>
)

In our routes.js, we've made the Post route a componentless route. It's still a child of the App route, but now has to completely rely on the App component for rendering. Below are the changes to App.js:

// components/App.js
...

render() {
   const posts = this.state.posts.map((post) => {
     const linkTo = `/${post.id}/${post.slug}`;

     return (
       <li key={post.id}>
         <Link to={linkTo}>{post.title}</Link>
       </li>
     )
   })

   const { postId, postName } = this.props.params;
   let postTitle, postContent
   if (postId && postName) {
     const post = this.state.posts.find(p => p.id == postId)
     postTitle = post.title
     postContent = post.content
   }

   return (
     <div>
       <h3>Posts</h3>
       <ul>
         {posts}
       </ul>

       {postTitle && postContent ? (
         <Post title={postTitle} content={postContent} />
       ) : (
         <h1>Welcome to the Universal Blog!</h1>
       )}
     </div>
   )
}
}

export default App

If we are on a post page, then props.params.postId and props.params.postName will both be defined and we can use them to grab the desired post and pass the data on to the Post component to be rendered. If those properties are not defined, then we're on the home page and can simply render a greeting. Now, our Post.js component can be a simple stateless functional component that simply renders its properties.

// components/Post.js
import React from 'react'

const Post = ({title, content}) => ( <div> <h3>{title}</h3> <p>{content}</p> </div>)

export default Post

With that refactoring complete, we're ready to implement data loading.

Step 6: Data Loading

git checkout data-loading && npm install

For this final step, we just need to make two small changes in server.js and App.js:

// server.js

...
app.get('*', (req, res) => {
match({ routes: routes, location: req.url }, (err, redirect, props) => {
   if (err) {
     res.status(500).send(err.message)
   } else if (redirect) {
     res.redirect(redirect.pathname + redirect.search)
   } else if (props) {
     const routerContextWithData = (
      <RouterContext
         {...props}
         createElement={(Component, props) => {
           return <Component posts={posts} {...props} />
         }}
       />
     )
     const appHtml = renderToString(routerContextWithData)
     res.send(renderPage(appHtml))
   } else {
     res.status(404).send('Not Found')
   }
})
})

...

// components/App.js
import React from 'react'
import Post from './Post'
import { Link, IndexLink } from 'react-router'

const allPostsUrl = '/api/post'

class App extends React.Component {
constructor(props) {
   super(props)
   this.state = {
     posts: props.posts || []
   }
}
  
...

In server.js, we're changing how the RouterContext creates elements by overwriting its createElement function and passing in our data as additional props. These props will get passed to any component that is matched by the route, which in this case will be our App component. Then, when the App component is initialized, it sets its posts state property to what it got from props or an empty array.

That's it! Run npm start one last time, and cruise through your app. You can even disable JavaScript, and the app will automatically degrade to requesting whole pages.

Thanks for reading!

build-universal-javascript-app-part-2-img-0

About the author

John Oerter is a software engineer from Omaha, Nebraska, USA. He has a passion for continuous improvement and learning in all areas of software development, including Docker, JavaScript, and C#. He blogs at here.