kriasoft / isomorphic-style-loader

CSS style loader for Webpack that is optimized for isomorphic (universal) web apps.
https://reactstarter.com
MIT License
1.27k stars 143 forks source link

Using with react-router #15

Open rvboris opened 8 years ago

rvboris commented 8 years ago

Hello, how to provide context to react-router?

--- Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/30635943-using-with-react-router?utm_campaign=plugin&utm_content=tracker%2F26439769&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F26439769&utm_medium=issues&utm_source=github).
michaseel commented 8 years ago

Same here. @rvboris Have you found a solution?

rvboris commented 8 years ago

@michaseel I use isomorphic-style-loader only on server build for class loading and extract-text-webpack on client build to generate css files

alphashuro commented 8 years ago

Also looking for a solution to this

frenzzy commented 8 years ago

Maybe something like this?

client.js

import React from 'react';
import ReactDOM from 'react-dom';
import WithStylesContext from './WithStylesContext';
import { Router, browserHistory } from 'react-router';
import routes from './routes';

ReactDOM.render(
  <WithStylesContext onInsertCss={styles => styles._insertCss()}>
    <Router history={browserHistory} routes={routes} />
  </WithStylesContext>,
  document.getElementById('root')
);

server.js

import express from 'express';
import React from 'react';
import ReactDOM from 'react-dom';
import WithStylesContext from './WithStylesContext';
import { Router, createMemoryHistory } from 'react-router';
import routes from './routes';

const server = express();
const port = process.env.PORT || 3000;

server.get('*', (req, res) => {
  const css = []; // CSS for all rendered React components
  const body = ReactDOM.renderToString(
    <WithStylesContext onInsertCss={styles => css.push(styles._getCss())}>
      <Router history={createMemoryHistory} routes={routes} />
    </WithStylesContext>
  );
  const html = `<!doctype html>
      <html>
        <head>
          <title>React Router with Isomorphic CSS Example</title>
          <style>${css.join('')}</style>
        </head>
        <body>
          <div id="root">${body}</div>
          <script src="/client.js"></script>
        </body>
      </html>`;
  res.status(200).send(html);
});

server.listen(port, () => {
  console.log(`Node.js app is running at http://localhost:${port}/`);
});

routes.js

import React from 'react';
import { Route, IndexRoute, Link } from 'react-router';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Layout.css';

function Layout({ children }) {
  return (
    <div className={s.root}>
      <header className={s.header}>
        <h1 className={s.title}>React Router with Isomorphic CSS Example</h1>
        <ul className={s.nav}>
          <li className={s.item}><Link to="/">Home</Link></li>
          <li className={s.item}><Link to="/foo">Foo</Link></li>
          <li className={s.item}><Link to="/bar">Bar</Link></li>
        </ul>
      </header>
      {children}
    </div>
  );
}

const LayoutWithStyles = withStyles(s)(Layout);
const Home = () => (<div>Home!</div>);
const Foo = () => (<div>Foo!</div>);
const Bar = () => (<div>Bar!</div>);

const routes = (
  <Route path="/" component={LayoutWithStyles}>
    <IndexRoute component={Home} />
    <Route path="foo" component={Foo} />
    <Route path="bar" component={Bar} />
  </Route>
);

export default routes;

WithStylesContext.js

import { Component, PropTypes, Children } from 'react';

export default class WithStylesContext extends Component {
  static propTypes = {
    children: PropTypes.element.isRequired,
    onInsertCss: PropTypes.func.isRequired,
  };

  static childContextTypes = {
    insertCss: PropTypes.func.isRequired,
  };

  getChildContext() {
    return { insertCss: this.props.onInsertCss };
  }

  render() {
    return Children.only(this.props.children);
  }
}

export default WithStylesContext;
chengjianhua commented 8 years ago

@frenzzy Thank you so much! As a fresher I have learned a lot from your resolution.

bjonamu commented 8 years ago

@frenzzy I tried your solution but I keep getting a syntax error [Your assistance will be greatly appreciated]

> 1 | .inp{
    | ^
  2 |     padding:10px;
  3 | }
  4 |

my client.js is as follows:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';

import WithStylesContext from '../shared/WithStylesContext';
import configureStore from '../shared/store/configureStore';
import { routes } from '../shared/routes';

const initialState = window.__INITIAL_STATE__;
const store = configureStore(browserHistory, initialState, window.devToolsExtension && window.devToolsExtension());
const history = syncHistoryWithStore(browserHistory, store);

ReactDOM.render(
  <Provider store={store}>
    <WithStylesContext onInsertCss={styles => styles._insertCss()}>
      <Router routes={routes} history={history} />
    </WithStylesContext>
  </Provider>,
  document.getElementById('app')
);

My server.js is as follows (I didn't include the imports here):

app.get('*', (req, res) => {
  const memoryHistory = createMemoryHistory(req.path);
  let store = configureStore(memoryHistory);
  const history = syncHistoryWithStore(memoryHistory, store);
  const css = []; // CSS for all rendered React components
  // routes is our object of React routes defined above
  match({ history, routes, location: req.url }, (err, redirectLocation, props) => {
    if (err) {
      // something went badly wrong, so 500 with a message
      res.status(500).send(err.message);
    } else if (redirectLocation) {
      // we matched a ReactRouter redirect, so redirect from the server
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (props) {
      const finalState = store.getState();
      store = configureStore(memoryHistory, finalState);
      // if we got props, that means we found a valid component to render
      // for the given route
      const markup = renderToString(
        <Provider store={store}>
          <WithStylesContext onInsertCss={styles => css.push(styles._getCss())}>
            <RouterContext {...props} />
          </WithStylesContext>
        </Provider>
        );
      // render `index.ejs`, but pass in the markup we want it to display
      const styl = css.join('');
      res.render('index', { markup, finalState, styl });
    } else {
      // no route match, so 404. In a real app you might render a custom
      // 404 view here
      res.sendStatus(404);
    }
  });
});

My index.ejs file has <style type="text/css"><%- styl %></style>

My webpack.config.js file has

      {
        test: /\.css$/,
        loaders: [
          'isomorphic-style-loader',
          'css-loader?modules&localIdentName=[name]_[local]_[hash:base64:3]',
          'postcss-loader',
        ],
      },

My Login.js view which I want to style has:

import React from 'react';
import { Link } from 'react-router';

import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Login.css';

class Login extends React.Component {
  constructor(props) {
    super(props);
    this.displayName = 'Login';
  }
  render() {
    return (
      <div className="container">
        <h1>Welcome to VISA eLearning</h1>
        <input type="email" placeholder="Email address" />
        <input type="password" placeholder="Password" />
        <p>
          <Link to="/forgot" className="btn btn-primary">Forgot Password</Link>
          {'  '}
          <Link to="/register" className="btn btn-primary">Create an account</Link>
        </p>
      </div>
    );
  }
}

export default withStyles(s)(Login);
frenzzy commented 8 years ago

@Jonrock23 you need to configure isomorphic-style-loader for the server bundle too (see example react-starter-kit/tools/webpack.config.js)

ed-sparkes commented 8 years ago

@frenzzy having some issue with your solution server side, i get styles._getCss is not a function, when i debug styles is an empty object {}, any idea what might be going wrong? Works fine on client side.

alex-shamshurin commented 8 years ago

For me it works with this config but only in development, in production styles._getCss is not a function

frenzzy commented 8 years ago

@alex-shamshurin sounds like you have different configuration for release mode, just use isomorphic-style-loader for release configuration too.

alex-shamshurin commented 8 years ago

On client

 {
        test  : /\.scss$/,
        loader: 'isomorphic-style-loader!css?modules&importLoaders=2&localIdentName=[path][name]__[local]___[hash:base64:5]!postcss-loader!sass?outputStyle=expanded&sourceMap'
      },    

On dev server:

 {
        test  : /\.scss$/,
        loader: 'isomorphic-style-loader!css?modules&importLoaders=2&localIdentName=[path][name]__[local]___[hash:base64:5]!postcss-loader!sass?outputStyle=expanded&sourceMap'
      },

on prod server

{
    test  : /\.scss$/,
    loader:  ExtractTextPlugin.extract('css?locals&modules!postcss-loader!sass?outputStyle=expanded&sourceMap')
  },

when I changed on prod server config to

 loader:  ExtractTextPlugin.extract('isomorphic-style-loader!css?locals&modules!postcss-loader!sass?outputStyle=expanded&sourceMap')

I got an error "Module build failed: TypeError: text.forEach is not a function"

And I feel that something is wrong here. On prod i'd like have separate css file (and may be only critical css inlined)

frenzzy commented 8 years ago

@alex-shamshurin you should not mix ExtractTextPlugin with IsomorphicStyleLoader, you need to choose one of them and use for all environments. If you want to use ExtractTextPlugin you don't need IsomorphicStyleLoader at all.

alex-shamshurin commented 8 years ago

So it's not possible to have styles inlined on development and extracted to css file on production? Why? As far as I remember people used original style-loader for devs and extract plugin for prod, but style-loader has errors with 'window' object on server and not universal. Then what a use of all that?

For example this works:

 {
    test  : /\.css$/,
    loader: ExtractTextPlugin.extract('style-loader', 'css-loader?module!postcss-loader')
  },
koistya commented 8 years ago

@alex-shamshurin there are a few issues with the original syle-loader and ExtractTextPlugin - (1) harder to configure (different configs for development/production builds), (2) doesn't work well with code-splitting, (3) more files for the browser to load (e.g. just bundle.js vs bundle.js + styles.css), (4) style-loader injects all the stules into the DOM, which is not good for performance (5) there could be conflicting styles in different React components, style-loader doesn't support that. For example, with isomoprhic-style-loader you have CheckoutPage component where you may have styles on body, html elements.. these styles are going to be injected into the dom, only right before the CheckoutPage component is mounted and removed from the dom right after the component is unmounted.

alex-shamshurin commented 8 years ago

So I needn't separate css file at all, do you mean that? But if there are too many syles for many different pages it can increase page loading time, or isomorphic-style-loader includes only styles that is required by page? (during server rendering) and then ehat happens during client routing?

koistya commented 8 years ago

Correct, you don't need separate .css files at all and in order to optimize the initial page load AND your application is big enough, you may want to use code splitting, each chunk of your app, in this case, will include its own piece of CSS.

alex-shamshurin commented 8 years ago

Should I care about replacing styles on client routing? Why react-starter-kit contains this in App component:

componentWillMount() {
    const { insertCss } = this.props.context;
    this.removeCss = insertCss(s);
  }

  componentWillUnmount() {
    this.removeCss();
  }
koistya commented 8 years ago

Usually, you just decorate your components with withStyles(styles1, styles2, MyComponent) (import withStyles from 'isomorphic-style-loader'). But in case with the App component, it's a little bit different, because React's context is set inside this App component and withStyles will not work with it.

It would be great to split the existing App component into two, the one that sets React's context and the other that has markup and CSS for the main layout (src/components/Layout) decorated with withStyles(s, Layout) higher-order component. A PR with this update is welcome!

koistya commented 8 years ago

@alex-shamshurin BTW, here is an example - the Layout component + the top-level component that sets React's context here.

thebigredgeek commented 8 years ago

https://github.com/thebigredgeek/isomorph-style-loader < There is an active fork here with the latest round of changes, including hoist-non-react-statics integration on the decorator

alex-shamshurin commented 8 years ago

@koistya What's the use of mount and unmount handlers there? And BTW why all styles are repeated twice? when component is mounting does it care about styles in head?

0x7aF777 commented 7 years ago

@koistya Hi, I also get similar error _getCss is not a function. Both webpack config of my client side and server side for css is :

loader: [
          'isomorphic-style-loader',
          'css?localIdentName=[hash:base64]&modules&importLoaders=1&sourceMap',
          'postcss-loader',
          'sass-loader'],

When I debug the style object, it is object like:

{  header: '_1gojyfyWXhhfHNYDmYH54L',
    logo: 'aH8URw6l9v-WpVuVTmyIw',
    active: '_35I5aW8-Hdns_JUIos6gUp', }

It seems that help functions _getCss and _insertCss are not added to the style object.

thedaniel commented 7 years ago

It seems that help functions _getCss and _insertCss are not added to the style object.

I am having the same issue.

cescoferraro commented 7 years ago

any boilerplate with react-router-v4 ?

oleksii-udovychenko commented 7 years ago

Same thing.

oleksii-udovychenko commented 7 years ago

I've realized that I didn't bundle my server-side code with webpack (stupid me). Anyways, anyone who is interested in minmal, framework-agnostic example of isomorphic-style-loader + css-loader + webpack can check this repo:samples-isomorphic-style-loader-server-side which I created for my mates at work.

So if you don't have _getCss or _insertCss functions - bundle your server side code with webpack.

claudijo commented 6 years ago

@frenzzy Thanks for the proposed solution to get isomorphic style loader to work with react router. I had some initial issues with not being able to provide multiple styles to withStyle and share a proposed fix, in case someone else comes here with similar problems...

In server.js

change

<WithStylesContext onInsertCss={styles => css.push(styles._getCss())}>

to

<WithStylesContext onInsertCss={(...styles) => { styles.forEach(style => inlineStyles.push(style._getCss())) }}>

in client.js

change

<WithStylesContext onInsertCss={styles => styles._insertCss()}>

to

<WithStylesContext onInsertCss={(...styles) => {styles.forEach(style => style._insertCss())}}>

NithishReddy commented 6 years ago

@frenzzy i have used your solution as above but it is giving error as below Cannot read property 'apply' of undefined at WithStyles.componentWillMount

My code : `renderToString( <WithStylesContext onInsertCss={styles => css.push(styles._getCss())}>

{routes()}
        </WithStylesContext>

)` Any help??