kennet postigo
kennet postigo

Reputation: 525

Using webpack and react-router for lazyloading and code-splitting not loading

I'm working on moving my react v0.14+ redux v3.0 + react-router v1.0 codebase from client-side rendering to server-side rendering using webpack v1.12 to bundle and code-split into chunks to load routes and components on-demand.

Im following and basing my setup on https://github.com/rackt/example-react-router-server-rendering-lazy-routes as I think it provides simplicity and great utilities. All day yesterday I have been working on moving to server-side rendering but I run into a few issues I haven't been able to solve and I'm not completely sure if they are because of webpack not being setup correctly, if am doing something wrong with react-router on the server/client or the routes config, or if its something I'm doing wrong with setting up redux that is causing these issues.

I run into the following issues:

  1. I'm able to load the initial page and everything works well but no other routes load and gives me GET http://localhost:3000/profile 404 (Not Found)
  2. The index/home page javascript works but all assets(css) are rendered as text/javascript so the styles don't show up unless they are inline.

webpack.config.js

var fs = require('fs')
var path = require('path')
var webpack = require('webpack')

module.exports = {

  devtool: 'source-map',

  entry: './client/client.jsx',

  output: {
    path: __dirname + '/__build__',
    filename: '[name].js',
    chunkFilename: '[id].chunk.js',
    publicPath: '/__build__/'
  },

  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        loader: 'babel-loader'
      }
    ]
  },

  plugins: [
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin({
      compressor: { warnings: false },
    }),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
    })
  ]

}

server.js

import http from 'http';
import React from 'react';
import {renderToString} from 'react-dom/server';
import { match, RoutingContext } from 'react-router';
import {Provider} from 'react-redux';
import configureStore from './../common/store/store.js';

import fs from 'fs';
import { createPage, write, writeError, writeNotFound, redirect } from './server-utils.js';
import routes from './../common/routes/rootRoutes.js';

const PORT = process.env.PORT || 3000;

var store = configureStore();
const initialState = store.getState();

function renderApp(props, res) {
  var markup = renderToString(
    <Provider store={store}>
      <RoutingContext {...props}/>
    </Provider>
  );
  var html = createPage(markup, initialState);
  write(html, 'text/html', res);
}

http.createServer((req, res) => {

  if (req.url === '/favicon.ico') {
    write('haha', 'text/plain', res);
  }

  // serve JavaScript assets
  else if (/__build__/.test(req.url)) {
    fs.readFile(`.${req.url}`, (err, data) => {
      write(data, 'text/javascript', res);
    })
  }

  // handle all other urls with React Router
  else {
    match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
      if (error)
        writeError('ERROR!', res);
      else if (redirectLocation)
        redirect(redirectLocation, res);
      else if (renderProps)
        renderApp(renderProps, res);
      else
        writeNotFound(res);
    });
  }

}).listen(PORT)
console.log(`listening on port ${PORT}`)

server-utils

Is the same as from the repo that I posted above example-react-router-server-rendering-lazy-routes just navigate to /modules/utils/server-utils.js in the repo.The only difference is the createPage function:

export function createPage(html, initialState) {
  return( `
  <!doctype html>
  <html>
    <head>
      <meta charset="utf-8"/>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="./../bower_components/Ionicons/css/ionicons.min.css">
      <link rel="stylesheet" href="./../dist/main.css">
      <title>Sell Your Soles</title>
    </head>
    <body>
      <div id="app">${html}</div>
      <script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};</script>
      <script src="/__build__/main.js"></script>
    </body>
  </html>
  `);
}

rootRoute.js

// polyfill webpack require.ensure
if (typeof require.ensure !== 'function') require.ensure = (d, c) => c(require)

import App from '../components/App.jsx'
import Landing from '../components/Landing/Landing.jsx'

export default {
  path: '/',
  component: App,
  getChildRoutes(location, cb) {
    require.ensure([], (require) => {
      cb(null, [
        require('./UserProfile/UserProfileRoute.js'),
        require('./UserHome/UserHomeRoute.js'),
        require('./SneakerPage/SneakerPageRoute.js'),
        require('./Reviews/ReviewsRoute.js'),
        require('./Listings/ListingsRoute.js'),
        require('./Events/EventsRoute.js')
      ])
    })
  },
  indexRoute: {
    component: Landing
  }
}

userProfileRoute.js

import UserProfile from '../../components/UserProfile/UserProfile.jsx';

export default {
  path: 'profile',
  component: UserProfile
}

client.js

import React from 'react';
import { match, Router } from 'react-router';
import { render } from 'react-dom';
import { createHistory } from 'history';
import routes from './../common/routes/rootRoutes.js';
import {Provider} from 'react-redux';
import configureStore from './../common/store/store.js';


const { pathname, search, hash } = window.location;
const location = `${pathname}${search}${hash}`;

const initialState = window.__INITIAL_STATE__;
const store = configureStore(initialState);



// calling `match` is simply for side effects of
// loading route/component code for the initial location
match({ routes, location }, () => {
  render(
    <Provider store={store}>
      <Router routes={routes} history={createHistory()} />
    </Provider>,
    document.getElementById('app')
  );
});

Upvotes: 7

Views: 9670

Answers (2)

mjrussell
mjrussell

Reputation: 615

I helped you out on discord, but I thought I'd post the answer here as well.

If you are using babel6 (instead of babel5) and using export default in your components, then you need to update your routes to the following:

getChildRoutes(location, cb) {
    require.ensure([], (require) => {
        cb(null, [
            require('./UserProfile/UserProfileRoute.js').default,
            require('./UserHome/UserHomeRoute.js').default,
            require('./SneakerPage/SneakerPageRoute.js').default,
            require('./Reviews/ReviewsRoute.js').default,
            require('./Listings/ListingsRoute.js').default,
            require('./Events/EventsRoute.js').default
        ])
    })
}

See this SO discussion for more details: Babel 6 changes how it exports default

Upvotes: 11

Grgur
Grgur

Reputation: 7382

If you happen to upgrade to Webpack 2 (+ tree shaking), you would use System.import instead of requires, which is incredibly useful.

Here's how:

import App from 'containers/App';
function errorLoading(err) {
  console.error('Dynamic page loading failed', err);
}
function loadRoute(cb) {
  return (module) => cb(null, module.default);
}
export default {
  component: App,
  childRoutes: [
    {
      path: '/',
      getComponent(location, cb) {
        System.import('pages/Home')
          .then(loadRoute(cb))
          .catch(errorLoading);
      }
    },
    {
      path: 'blog',
      getComponent(location, cb) {
        System.import('pages/Blog')
          .then(loadRoute(cb))
          .catch(errorLoading);
      }
    }
  ]
};

You can get the entire guide in this blog post: Automatic Code Splitting for React Router

Upvotes: 3

Related Questions