React with CSS-modules

(or how I learned to stop using inline styles)

Ramsay Lanier

First, Why are inline styles a thing?

Scoped Styling

CSS is globally scoped. Styles are accessible anywhere. That means that changing a style could have unintended consequences. Or you have become increasingly more specific with your styles.

Which leads to...

Specificity Conflicts

Image courtesy of Smashing Magazine

DEAD CODE

We've all seen this before. This case is a minor infraction, but in the aggregate it can be harmful. 

These are all really bad things and should be dealt with

But we got problems

Like

hover states?

pseudo selectors?

class composition?

Sass stuff?

enter radium

Radium is cool

var Radium = require('radium');
var React = require('react');
var color = require('color');

@Radium
class Button extends React.Component {
  static propTypes = {
    kind: React.PropTypes.oneOf(['primary', 'warning']).isRequired
  };

  render() {
    // Radium extends the style attribute to accept an array. It will merge
    // the styles in order. We use this feature here to apply the primary
    // or warning styles depending on the value of the `kind` prop. Since its
    // all just JavaScript, you can use whatever logic you want to decide which
    // styles are applied (props, state, context, etc).
    return (
      <button
        style={[
          styles.base,
          styles[this.props.kind]
        ]}>
        {this.props.children}
      </button>
    );
  }
}

// You can create your style objects dynamically or share them for
// every instance of the component.
var styles = {
  base: {
    color: '#fff',

    // Adding interactive state couldn't be easier! Add a special key to your
    // style object (:hover, :focus, :active, or @media) with the additional rules.
    ':hover': {
      background: color('#0074d9').lighten(0.2).hexString()
    }
  },

  primary: {
    background: '#0074D9'
  },

  warning: {
    background: '#FF4136'
  }
};

but how would you replicate something like :first-of-type?

like this?

var Radium = require('radium');
var React = require('react');
var color = require('color');

@Radium
class View extends React.Component {

  render() {

    const { buttons } = this.props;

    return (

      {buttons.map( (button, index) => {
        const style = index === 0 ? styles.first : styles.base;
        return (
          <button
            style={style}>
            {this.props.children}
          </button>
        )
      })}
    );
  }
}

var styles = {
  base: {
    color: '#fff',
    ':hover': {
      background: color('#0074d9').lighten(0.2).hexString()
    }
  },

  first{
    background: 'green',
    ':hover': {
      background: color('green').lighten(0.2).hexString()
    }
  }
};

With CSS Modules

var React = require('react');
import CSSModules from 'react-css-modules';
import styles from './button.scss';

@CSSModules(styles, {allowMultiple: true})
class View extends React.Component {

  render() {

    const { buttons } = this.props;

    return (

      {buttons.map( button => {
        return (
          <button styleName="base">{button}</button>
        )
      })}
    )
  }
}


//buttons.scss

.base{
    color: white

    &:hover{
        background-color: lighten(green, .2);
    }


    &:first-of-type{
        background-color: lighten(red, .2);
    }
}

this is hard to swallow.

react css modules are so much better.

The setup

WEbpack

Node

Express

Webpack - Development

'use strict';

var path = require('path');
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');

const devServer = {
    contentBase: path.resolve(__dirname, './app'),
    outputPath: path.join(__dirname, './dist'),
    colors: true,
    quiet: false,
    noInfo: false,
    publicPath: '/',
    historyApiFallback: false,
    host: '127.0.0.1',
    port: 3000,
    hot: true
};

module.exports = {
  devtool: 'eval-source-map',
  debug: true,
  devServer: devServer,
  entry: [
    'webpack/hot/dev-server',
    'webpack-hot-middleware/client?reload=true',
    path.join(__dirname, 'app/main.js')
  ],
  output: {
    path: path.join(__dirname, '/dist/'),
    filename: '[name].js',
    publicPath: devServer.publicPath
  },
  module: {
    loaders: [
      {
        test: /\.js?$/,
        loader: 'babel',
        exclude: /node_modules|lib/,
      },
      {
        test: /\.json?$/,
        loader: 'json'
      },
      {
        test: /\.css$/,
        loader: 'style!css?modules&localIdentName=[name]---[local]---[hash:base64:5]'
      },
      {
        test: /\.scss$/,
        loaders: [
          'style?sourceMap',
          'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]',
          'sass?sourceMap'
        ],
        exclude: /node_modules|lib/
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'app/index.tpl.html',
      inject: 'body',
      filename: 'index.html'
    }),
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('dev')
    })
  ],
  node: {
    fs: 'empty'
  }
};

webpack.config.js

Webpack - Production

'use strict';

var path = require('path');
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var StatsPlugin = require('stats-webpack-plugin');

module.exports = {
  entry: [
    path.join(__dirname, 'app/main.js')
  ],
  output: {
    path: path.join(__dirname, '/dist/'),
    filename: '[name]-[hash].min.js'
  },
  plugins: [
    new ExtractTextPlugin('/app.min.css', {
      allChunks: true
    }),
    new HtmlWebpackPlugin({
      template: 'app/index.tpl.html',
      inject: 'body',
      filename: 'index.html'
    }),
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
        screw_ie8: true
      }
    }),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    })
  ],
  module: {
    loaders: [
      {
        test: /\.js?$/,
        loader: 'babel',
        exclude: /node_modules|lib/,
      },
      {
        test: /\.json?$/,
        loader: 'json'
      },
      {
        test: /\.css$/,
        loader: 'style!css?modules&localIdentName=[name]---[local]---[hash:base64:5]'
      },
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]!sass'),
        exclude: /node_modules|lib/,
      },
    ],
  }
};

webpack.production.config.js

Node w/ express

/* eslint no-console: 0 */
import path from 'path';
import express from 'express';
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import webpackMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import config from './webpack.config.js';

const isDeveloping = process.env.NODE_ENV !== 'production';
const APP_PORT = isDeveloping ? 3000 : process.env.PORT || 3000;

let app = express();

if (isDeveloping) {

  const compiler = webpack(config);

  app = new WebpackDevServer(compiler, {
    hot: true,
    historyApiFallback: true,
    contentBase: 'src',
    publicPath: config.output.publicPath,
    stats: {
      colors: true,
      hash: false,
      timings: true,
      chunks: false,
      chunkModules: false,
      modules: false
    }
   });

   app.use(webpackHotMiddleware(compiler));

} else {
  app.use(express.static('./dist'));
  app.get('*', function response(req, res, next) {
    res.sendFile(path.join(__dirname, '/index.html'));
  });
}

app.listen(APP_PORT, () => {
  console.log(`App is now running on http://localhost:${APP_PORT}`);
});

server.js

example time

import React from 'react';
import ReactDOM from 'react-dom';

import styles from './App.scss';

class App extends React.Component {

  render() {
    const { children } = this.props;

    return (
      <div className="application">
        {children}
      </div>
    )
  }
}

export default App;
@import "./styles/reset";
@import "./styles/fonts";
@import "./styles/vars";
@import "./styles/typeplate/typeplate";
@import "./styles/z-index";

*{
  box-sizing: border-box;
}

body{
  font-family: $sansSerif;
  font-weight: 400;
  background-color: darken(white, 10%);
}

a{
  text-decoration: none;
}
import React from 'react';
import Page from './page.js';
import Section from '../sections/section.js';

class HomePage extends React.Component{

  render(){
    return (
      <Page>
        <Section title="Ramsay" type="primary"></Section>
        <Section title="Work" type="secondary"></Section>
        <Section title="Play" type="dark"></Section>
        <Section title="Hire" type="tertiary" backgroundImage="http://red-badger.com/blog/wp-content/uploads/2015/04/react-logo-1000-transparent.png"></Section>
      </Page>
    )
  }
  
}

export default HomePage;
import React from 'react';

import CSSModules from 'react-css-modules';
import styles from './section.scss';

@CSSModules(styles, {allowMultiple: true})
class Section extends React.Component{

  _renderPostTitle(){
    const title = this.props.title.match(/.{1,2}/g);
    return title.map( (fragment, index) => {
      return <span key={index}>{fragment}<br></br></span>
    })
  }

  render(){

    const { type, backgroundImage } = this.props;
    const bg = {
      backgroundImage: "url('" + backgroundImage + "')",
    };

    const bgClass = backgroundImage ? 'with_background' : '';

    return(
      <div styleName={type + ' ' + bgClass} style={bg}>
        <h2 styleName="title">{this._renderPostTitle()}</h2>
        {this.props.children}
      </div>
    )
  }
}

export default Section;
.base{
  composes: child_1 container justify_center from '../../styles/flexbox.scss';
  position: relative;
}

.primary{
  composes: base;
  composes: priamry primary_bg from '../../styles/colors.scss';
}

.secondary{
  composes: base;
  composes: secondary_bg from '../../styles/colors.scss';
}

.dark{
  composes: base;
  composes: dark_bg from '../../styles/colors.scss';
}

.tertiary{
  composes: base;
  composes: tertiary_bg from '../../styles/colors.scss';
}

.with_background{
  composes: cover center from '../../styles/background.scss';
  position: relative;

  &:after{
    background-color: fade-out(black, .65);
  }
}

.title{
  composes: strong uppercase center bold from '../../styles/typography.scss';
  composes: self_center from '../../styles/flexbox.scss';
  // background-color: fade-out(white, .2);
  border: 10px solid fade-out(black, .8);
  color: white;
  font-size: 6vw;
  line-height: 5vw;
  padding: 1rem 2rem;
}

QUESTIONS

React with CSS Modules

By Ramsay Lanier

React with CSS Modules

Stop using inline styles. They're whack.

  • 1,777