markdalgleish / static-site-generator-webpack-plugin

Minimal, unopinionated static site generator powered by webpack
MIT License
1.61k stars 97 forks source link

Issue When Using React Router With Different Components #26

Open TerrellV opened 8 years ago

TerrellV commented 8 years ago

Description

With the router defined in the code below, I'm attempting to have each route render the html from its specified component. My current understanding is that the "/" path should render the Base component and the IndexRoute should cause the About component to also load at the "/' path.

NOTE: I'm using react-router 1.0.2

// Routes.jsx
import React from 'react';
import {Router, Route, IndexRoute} from "react-router";

import {Portfolio} from './components/portfolio.js';
import {About} from './components/about.js';
import {Base} from './components/base.js';
import {Blog} from './components/blog.js';
// let About = require('./components/index.js')

let Routes = (
    <Route path="/" component={Base}>
      <IndexRoute component={About} />
      <Route path="/portfolio" component={Portfolio}/>
      <Route path="/blog" component={Blog}/>
    </Route>
)

export {Routes};

Original issue (Solved):

After running webpack the website generator plugin creates static pages in the correct location but they all end up with the same html. I defined a different component to each path just as a test yet they all render the component or html from the top level route (in this case "Base").

Below is my Entry file

entry.js

import path  from 'path';
import React from "react";
import ReactDOMServer from 'react-dom/server';
import {EventEmitter} from "events";
import { createHistory, createMemoryHistory } from 'history';
import { Router, RoutingContext, match } from 'react-router';

import {Portfolio} from './components/portfolio.js';
import {About} from './components/about.js';
import {Base} from './components/base.js';
import {Routes} from './routes.js';

module.exports = function render(locals, callback) {
  let history = createMemoryHistory(locals.path);

  let html = ReactDOMServer.renderToStaticMarkup(
    React.createElement(Router, {'history': history}, Routes )
  );

  callback(null, html);
};

Finally here is my webpack.config.js file

var path = require('path');
var webpack = require('webpack');
var data = require('./data.js');
var StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin');

module.exports = {
  entry: './dev/app/entry.js',

  output: {
    filename: 'bundle.js',
    path: __dirname,
    libraryTarget: 'umd'
  },

  module: {
      loaders:[
        {
          test: /\.js$/,
          exclude: /(node_modules)/,
          loaders:['babel?presets[]=es2015,presets[]=react'],
        }
      ]
    },

  plugins: [
    new StaticSiteGeneratorPlugin('bundle.js', data.routes, data)
  ]
}

I'm not sure what I'm doing wrong. I've tried to follow the react router example on the README but I'm having trouble understanding exactly what the default render function is doing. How is match used ??? And the template function ??

Also, I'm unsure of how the required files are setup.

I am really looking forward to finally figuring this out. I've spent a long time on it. I appreciate if you are able to bring some clarification in setting things up correctly with different html components.

Please let me know if anything is unclear or additional information is needed.


Update

After some more grinding through the code I figured out my issue. I can't say I completely understand it as some of it has to do with how React Router is written but It works. I can dive more into React Router code later if need be.

First my router top level component needed to use {{this.props.children}} in the rendered Base Component that I have referenced. This way it will also render the IndexRoute component inside of the top level or "Base" Component.

Next I needed to change my render function to look like this. I wasn't using

<RoutingContext {...renderProps} />

Also the es6 destructuring syntax was not as it should be considering how I had my routes component being imported. I simply changed {Routes, location} to {routes: Routes, location}.

New Entry.js file

import path  from 'path';
import React from "react";
import ReactDOMServer from 'react-dom/server';
import {EventEmitter} from "events";
import { createHistory, createMemoryHistory } from 'history';
import { Router, RoutingContext, match } from 'react-router';
import Routes from './routes.js';

// version with match...
module.exports = function render(locals, callback) {
  let history = createMemoryHistory(locals.path);
  let location = history.createLocation(locals.path);

  match( { routes:Routes, location:location }, (error, redirectLocation, renderProps) => {

    function generateHTML() {
      return ReactDOMServer.renderToStaticMarkup(
        <RoutingContext {...renderProps} />
      )
    }

    callback(null, generateHTML());
  })

};

New Problem

How do I setup "live reloading" with scss that is being applied to the statically generated components? Currently I have my styles being extracted into a separate css file. Unfortunately I've read that there is no support for hot reloading the styles this way.... How do I setup hot reloading with scss that needs to be applied to components that will be statically rendered ? Keep in mind I tried using the traditional method of style!css!sass loader but I run into the issue of the Window not being defined because it executes before its in the browser environment. Suggestions / solutions welcomed. Right now I have to manually refresh my page to style the react components being statically rendered.

VaclavSynacek commented 8 years ago

I have no idea why you or the README is using match and RoutingContext. But if you are just struggling to put together the first working example of static-site-generator-webpack-plugin you may take inspiration in VaclavSynacek/react-isomorphic-static-site-generator-boilerplate. It's not perfect, but it will get you going.

TerrellV commented 8 years ago

@VaclavSynacek I did it because it was the most relevant example I could find. Thank you for including the link. How would one process sass and apply it via the style-loader in such a configuration? I figured out how to do this with the browser/client side js but I get errors for obvious reasons when I try and require those styles in the react components that get converted to static html.

What's the best way to process sass and apply it to the components being rendered statically ? Because of the situation is the only option to use ExtractTextPlugin ? Id really like to use styl-loader for its development benefits but it requires the window.

cusxio commented 8 years ago

I assume you want to pass custom props such as your assets file from locals.assets.

One way to do it to create a wrapper using Context.

Wrapper:

class Wrapper extends React.Component {
    getChildContext() {
        return {assets: this.props.assets};
    }
    render() {
        return this.props.children;
    }
}
Wrapper.childContextTypes = {
    assets: React.PropTypes.string.isRequired
};

To use it:

export default(locals, callback) => {
    const history = createMemoryHistory();
    const location = history.createLocation(locals.path);

    match({routes: routes,location: location}, (error, redirectLocation, renderProps) => {
        const assets = locals.assets.main;
        if (renderProps) {
            let html = renderToStaticMarkup(<Wrapper assets={assets}><RoutingContext {...renderProps}/></Wrapper>);
            callback(null, html)
        }
    })
};

In your _matched route component_:

export default class Root extends Component {
    render() {
        return (
            <html>
                <head>
                    <script src={this.context.data}></script>
                </head>
                <body>
                </body>
            </html>
        );
    }
}

Root.contextTypes = {
    assets: React.PropTypes.string
}
TerrellV commented 8 years ago

@cusxio thank you for your response, after some time, I realized that I couldn't find a logical reason as to why I would need custom props when rendering components statically. I thought I would need them for functionality but all the interaction on my site will be implemented via client side react code where I can insert props and such like normal. My current issue however revolves around styling. I updated the original issue at the top to describe the current problem if you are interested in still assisting.

Also, if there is a valid reason to provide custom props to a statically generated component please let me know and I will look into the code you posted. Thanks again for taking the time to provide input. It is much appreciated.

cusxio commented 8 years ago

@MirPresT I believe you can't use this plugin in development mode to hot-reload CSS/JS. This plugin is used to create static assets. (i.e for production).

What you can do is set up two webpack configs. One for production where this plugin lies, and one for development.

To enable hot-reloading in development you need to run an express server serving your assets from webpack. There are a few examples on GitHub, but the idea is the same - host an express server (do some serve side rendering), and serve webpack assets.

Examples:

TerrellV commented 8 years ago

@cusxio Thank you. I currently have two configs but for a different reason but I never thought to separate them for the reason you mentioned. So without changing the files too much I could in theory add an if statement in the entry.js file and if I'm running webpack for production I can render the static html and if I'm running webpack for development I can render the normal react component as I would normally.

And then of course as you mentioned just remove the plugin from the dev config file and setup hot loading in my server etc.

Am I missing anything ?

cusxio commented 8 years ago

That is pretty much it, but when your on development, you have to find a way to render to the dom.

This blog post explains pretty much the problems you are facing and how to use React to solve it.