-
Notifications
You must be signed in to change notification settings - Fork 144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to use isomorphic-style-loader effectively with component libraries? #62
Comments
You can write a webpack-loader which will wrap all |
@jussch So why not to wrap the most outer component with library styles? I do. |
@javivelasco That's an issue you might be interested in. I'm really curious about how we could handle theming with lazy-loaded / injected style. I have never found time to look into this but it's often a blocker when builder large scale app. |
@frenzzy Thanks for the tip, but unless I'm mistaken, there are a couple issues:
I was hoping there was a more convenient solution, especially one that would translate well into integrating @alex-shamshurin are you suggesting loading in React-Toolbox's components styles each time i use them, like so: import Button from 'react-toolbox/lib/button';
import buttonStyles from 'react-toolbox/lib/button/theme.scss';
import FontIcon from 'react-toolbox/lib/font_icon';
import fontIconStyles from 'react-toolbox/lib/font_icon/theme.scss';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
function MyOwnComponent () {
return (
<div>
<FontIcon></FontIcon>
<Button></Button>
</div>
)
}
export default withStyles(buttonStyles, fontIconStyles)(MyOwnComponent) If so, it's more inconvenient because of how often I use react-toolbox components. I also noticed while playing around with // from react-starter-kit
const context = {
insertCss: (...styles) => {
const removeCss = styles.map(x => x._insertCss());
return () => { removeCss.forEach(f => f()); };
},
}; That this actually redefines the CSS order that you'd normally get with Example: // File child.jsx
import childStyle from './childStyle.css';
export default withStyles(childStyle)(function ChildComponent({ className }) {
return <div className={childStyle.normal + ' ' + className} />;
});
// File parent.jsx
import ChildComponent from './child'
import parentStyle from './parentStyle.css';
export default withStyles(parentStyle)(function ParentComponent() {
return <ChildComponent className={parentStyle.overwrite} />;
}); With This means to overwrite a third-party component styles (or any child component's styles) I have to beat the specificity of any selectors that are used. This can get nasty fast, especially if there is a large chain of style overwrites. I can submit this problem as a separate issue, but it seemed relevant here. |
If you want to override external component style maybe it is a good idea to do it like so: /* MyComponent.css */
@import 'external/component.css';
.externalClassName {
color: red; /* override color for class name from external/component.css */
} |
Interesting, what do you do with /* ./Button/index.js */
import Button from 'react-toolbox/lib/button';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import buttonStyles from './overwrite.css';
export default withStyles(buttonStyles)(Button); /* ./Button/overwrite.css */
@import 'react-toolbox/lib/button/theme.css';
.someSelectorToOverwrite {
color: red;
} If so, I don't believe that works. The original button component at |
I would also be interestes in how to integrate this loader with react toolbox. Regards |
There you go, fresh out out of the oven. Use it instead of withStyles. It expects the same arguments as themr() function from react-css-themr, except it has isomorphic-style-loader superpowers :) Theming react-toolbox components is very easy with react-css-themr. Read react-toolbox installation guide to see how to set it up with themr. Besides themr is very nice to use for styling your own components as well. You can even switch themes at runtime with ThemeProvider. (Although there is a bug (javivelasco/react-css-themr#34), which can be fixed to make componentWillReceiveProps() method of themr() function track also changes to the context. I'm too lazy to do it myself, personally I don't need runtime theme switching right now. The code hasn't been tested properly so please be gentle. Let me know if you spot any bugs. import React, { Component, PropTypes } from 'react';
import hoistStatics from 'hoist-non-react-statics';
import { themr } from 'react-css-themr';
const CONTEXT_THEME = 'CONTEXT_THEME';
const PROPS_THEME = 'PROPS_THEME';
const DEFAULT_THEME = 'DEFAULT_THEME';
export default (componentName, defaultTheme, options) => (ComposedComponent) => {
const ThemrComponent = themr(
componentName, defaultTheme, options
)(ComposedComponent);
const displayName = ComposedComponent.displayName
|| ComposedComponent.name
|| 'Component';
class WithThemr extends Component {
static displayName = `WithThemr(${displayName})`;
static contextTypes = {
themr: PropTypes.object,
insertCss: PropTypes.func,
}
static propTypes = {
...ThemrComponent.propTypes,
}
static defaultProps = {
...ThemrComponent.defaultProps,
}
constructor(props) {
super(props);
this.removeThemes = {};
}
componentWillMount() {
if (this.props.composeTheme) {
// composeTheme option is not false, meaning styles of the component
// are composed of Context, Default and Props themes.
const contextTheme = this.context.themr.theme[componentName];
this.insertTheme(CONTEXT_THEME, contextTheme);
this.insertTheme(DEFAULT_THEME, defaultTheme);
}
// Props theme has the highest priority of the three and needs to be
// inserted regardless of the composeTheme option.
this.insertTheme(PROPS_THEME, this.props.theme);
}
componentWillReceiveProps(nextProps, nextContext) {
const contextTheme = this.context.themr.theme[componentName];
const nextContextTheme = nextContext.themr.theme[componentName];
if (nextProps.composeTheme) {
// composeTheme option is not false, meaning styles of the component
// are composed of Context, Default and Props themes.
if (!this.props.composeTheme) {
// composeTheme option used to be false, insert Context and Default
// themes.
this.insertTheme(CONTEXT_THEME, nextContextTheme);
this.insertTheme(DEFAULT_THEME, defaultTheme);
} else if (nextContextTheme !== contextTheme) {
// Context theme has changed, replace it.
this.removeTheme(CONTEXT_THEME);
this.insertTheme(CONTEXT_THEME, nextContextTheme);
}
if (nextProps.theme !== this.props.theme) {
// Props theme has changed, replace it.
this.removeTheme(PROPS_THEME);
this.insertTheme(PROPS_THEME, nextProps.theme);
}
} else {
// composeTheme option is false, meaning styles of the component
// are not composed and are provided by a theme with the highest priority.
if (nextProps.theme || defaultTheme) {
// Props theme and Default theme have higher priority than Context
// theme. When either of them exist, we need to remove Context theme.
this.removeTheme(CONTEXT_THEME);
}
if (nextProps.theme) {
// Props theme has a higher priority than Default theme.
// When it exists, we need to remove Default theme.
this.removeTheme(DEFAULT_THEME);
}
}
}
componentWillUnmount() {
this.removeTheme(CONTEXT_THEME);
this.removeTheme(DEFAULT_THEME);
this.removeTheme(PROPS_THEME);
}
insertTheme(type, theme) {
if (theme) {
this.removeThemes[type] = this.context.insertCss.apply(
undefined, [theme]
);
}
}
removeTheme(type) {
if (this.removeThemes[type]) {
setTimeout(this.removeThemes[type], 0);
}
}
render() {
return <ThemrComponent {...this.props} />;
}
}
WithThemr.ComposedComponent = ThemrComponent;
return hoistStatics(WithThemr, ComposedComponent);
}; |
Note that you would still have to wrap the react-toolbox components like this though: import { Button } from 'react-toolbox/lib/button/Button';
import withThemr from './withThemr'; // the code I posted above
export default withThemr('RTButton')(Button); and pass the following context theme object to ThemeProvider just like you would with react-css-themr: {
/* eslint-disable global-require */
RTButton: require('react-toolbox/lib/button/theme.scss'),
/* eslint-enable global-require */
}
It's not a perfect solution, but it makes much easier to meet the styling requirements of different components in your app. |
Just the same case. Maybe a babel-plugin or webpack plugin will be the best solution. |
@jazblueful First of all, thanks for providing that Code for the curious:theme.js
button.js
RTComponent.js
|
To solve the same problem, I checked the And finally, I wrote my own loader It does not have to write (Project react-starter-kit) // webpack.config.js
const REACT_TOOLBOX_PATH = path.resolve(__dirname, '../node_modules/react-toolbox')
// Rules for Style Sheets
{
test: reStyle,
rules: [
// Convert CSS into JS module
{
issuer: { not: [reStyle, REACT_TOOLBOX_PATH] },
use: 'isomorphic-style-loader',
},
// Handle react-toolbox style alone since it cannot intergrate with isomorphic-style-loader well.
{
issuer: REACT_TOOLBOX_PATH,
use: 'iso-morphic-style-loader'
},
// Process external/third-party styles
{
exclude: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
loader: 'css-loader',
options: {
sourceMap: isDebug,
minimize: !isDebug,
discardComments: { removeAll: true },
},
},
// Process internal/project styles (from src folder)
{
include: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
use: CSS_LOADERS
}
]
}, And if you need ssr (almost the same as react-starter-kit): // server.js
// styles handled by iso-morphic-style-loader
if (global.__universal__) {
global.__universal__.forEach(v => {
data.styles.push({
...v.attrs,
id: v.id,
cssText: v.content.join('\n')
})
})
}
// html.js
{styles.map(({id, cssText, ...rest}) =>
<style
{...rest}
key={id}
id={id}
dangerouslySetInnerHTML={{ __html: cssText }}
/>
)} |
@creeperyang Interesting. I'll have to take a look at it. Out of curiosity, when you need the RT component which way do you import? Like this: Also, when you say there's no longer a need for |
Yeah, it's what I mean.
In my own project, I prefer |
hmm... still getting the unexpected token error:
Here's the relevant webpack config if that helps:
Thanks for trying to help me with this 🙂 |
@wootencl It looks like that you should handle // Rules for JS / JSX
{
test: reScript,
include: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
loader: 'babel-loader',
options: {
// https://github.com/babel/babel-loader#options
cacheDirectory: isDebug,
// https://babeljs.io/docs/usage/options/
babelrc: false,
presets: [
// A Babel preset that can automatically determine the Babel plugins and polyfills
// https://github.com/babel/babel-preset-env
[
'env',
{
targets: {
browsers: BROWSER_LIST,
uglify: true,
},
modules: false,
useBuiltIns: false,
debug: false
}
],
// Experimental ECMAScript proposals
// https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-
'stage-2',
// JSX, Flow
// https://github.com/babel/babel/tree/master/packages/babel-preset-react
'react',
// Optimize React code for the production build
// https://github.com/thejameskyle/babel-react-optimize
...(isDebug ? [] : ['react-optimize']),
],
plugins: [
// Adds component stack to warning messages
// https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-source
...(isDebug ? ['transform-react-jsx-source'] : []),
// Adds __self attribute to JSX which React will use for some warnings
// https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-self
...(isDebug ? ['transform-react-jsx-self'] : []),
]
}
}, My whole `webpack.config.js`import path from 'path'
import webpack from 'webpack'
import AssetsPlugin from 'assets-webpack-plugin'
import nodeExternals from 'webpack-node-externals'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import pkg from '../package.json'
const isDebug = !process.argv.includes('--release')
const isVerbose = process.argv.includes('--verbose')
const isAnalyze =
process.argv.includes('--analyze') || process.argv.includes('--analyse')
const reScript = /\.jsx?$/
const reStyle = /\.(css|less|scss|sss)$/
const reFont = /\.(ttf|eot|woff2?)$/
const reImage = /\.(bmp|gif|jpe?g|png|svg)$/
const staticAssetName = isDebug
? '[path][name].[ext]?[hash:8]'
: '[hash:8].[ext]'
const BROWSER_LIST = [
'>1%',
'last 3 versions',
'Firefox ESR',
'not ie < 9'
]
const REACT_TOOLBOX_PATH = path.resolve(__dirname, '../node_modules/react-toolbox')
const CSS_LOADERS = [
{
loader: 'css-loader',
options: {
// CSS Loader https://github.com/webpack/css-loader
importLoaders: 1,
sourceMap: isDebug,
// CSS Modules https://github.com/css-modules/css-modules
modules: true,
localIdentName: isDebug
? '[name]-[local]-[hash:base64:5]'
: '[hash:base64:5]',
// CSS Nano http://cssnano.co/options/
minimize: !isDebug,
discardComments: { removeAll: true },
}
},
{
loader: 'postcss-loader',
options: {
plugins: (loader) => [
require('postcss-cssnext')({
// Custom react-toolbox theme
features: {
customProperties: {
variables: {
'--appbar-color': '#3293d2',
'--tab-pointer-color': '#3293d2',
'--input-text-highlight-color': '#3293d2',
'--preferred-font': `-apple-system,Helvetica,Arial,Tahoma,"PingFang SC","Hiragino Sans GB","Lantinghei SC","Microsoft YaHei",sans-serif`
}
}
}
})
]
}
}
]
const config = {
context: path.resolve(__dirname, '..'),
output: {
path: path.resolve(__dirname, '../build/public/assets'),
publicPath: '/assets/',
pathinfo: isVerbose,
filename: isDebug ? '[name].js' : '[name].[chunkhash:8].js',
chunkFilename: isDebug
? '[name].chunk.js'
: '[name].[chunkhash:8].chunk.js',
devtoolModuleFilenameTemplate: info =>
path.resolve(info.absoluteResourcePath),
},
resolve: {
// Allow absolute paths in imports, e.g. import Button from 'components/Button'
// Keep in sync with .flowconfig and .eslintrc
modules: ['node_modules', 'src'],
alias: {
'fonts.css': path.resolve(__dirname, '../public/styles/fonts.css')
}
},
module: {
// Make missing exports an error instead of warning
strictExportPresence: true,
rules: [
// Rules for JS / JSX
{
test: reScript,
include: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
loader: 'babel-loader',
options: {
// https://github.com/babel/babel-loader#options
cacheDirectory: isDebug,
// https://babeljs.io/docs/usage/options/
babelrc: false,
presets: [
// A Babel preset that can automatically determine the Babel plugins and polyfills
// https://github.com/babel/babel-preset-env
[
'env',
{
targets: {
browsers: BROWSER_LIST,
uglify: true,
},
modules: false,
useBuiltIns: false,
debug: false
}
],
// Experimental ECMAScript proposals
// https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-
'stage-2',
// JSX, Flow
// https://github.com/babel/babel/tree/master/packages/babel-preset-react
'react',
// Optimize React code for the production build
// https://github.com/thejameskyle/babel-react-optimize
...(isDebug ? [] : ['react-optimize']),
],
plugins: [
// Adds component stack to warning messages
// https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-source
...(isDebug ? ['transform-react-jsx-source'] : []),
// Adds __self attribute to JSX which React will use for some warnings
// https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-self
...(isDebug ? ['transform-react-jsx-self'] : []),
]
}
},
// Rules for Style Sheets
{
test: reStyle,
rules: [
// Convert CSS into JS module
{
issuer: { not: [reStyle, REACT_TOOLBOX_PATH] },
use: 'isomorphic-style-loader',
},
// Handle react-toolbox style since it cannot intergrate with isomorphic-style-loader well.
{
issuer: REACT_TOOLBOX_PATH,
use: 'iso-morphic-style-loader'
},
// Process external/third-party styles
{
exclude: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
loader: 'css-loader',
options: {
sourceMap: isDebug,
minimize: !isDebug,
discardComments: { removeAll: true },
},
},
// Process internal/project styles (from src folder)
{
include: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
use: CSS_LOADERS
}
]
},
// Rules for fonts
{
test: reFont,
use: [
{
loader: 'url-loader',
options: {
name: staticAssetName,
limit: 1 // always use url instead of base64
}
}
]
},
// Rules for images
{
test: reImage,
oneOf: [
// Inline lightweight images into CSS
{
issuer: reStyle,
oneOf: [
// Inline lightweight SVGs as UTF-8 encoded DataUrl string
{
test: /\.svg$/,
loader: 'svg-url-loader',
options: {
name: staticAssetName,
limit: 4096 // 4kb
}
},
// Inline lightweight images as Base64 encoded DataUrl string
{
loader: 'url-loader',
options: {
name: staticAssetName,
limit: 4096 // 4kb
}
}
]
},
// Or return public URL to image resource
{
loader: 'file-loader',
options: {
name: staticAssetName,
}
}
]
},
{
test: /lottie_files\/.+\.json$/,
loader: 'file-loader',
options: {
name: staticAssetName
}
},
// Exclude dev modules from production build
...(isDebug
? []
: [
{
test: path.resolve(
__dirname,
'../node_modules/react-deep-force-update/lib/index.js',
),
loader: 'null-loader',
},
])
]
},
// Don't attempt to continue if there are any errors.
bail: !isDebug,
cache: isDebug,
// Specify what bundle information gets displayed
// https://webpack.js.org/configuration/stats/
stats: {
cached: isVerbose,
cachedAssets: isVerbose,
chunks: isVerbose,
chunkModules: isVerbose,
colors: true,
hash: isVerbose,
modules: isVerbose,
reasons: isDebug,
timings: true,
version: isVerbose
},
// Choose a developer tool to enhance debugging
// https://webpack.js.org/configuration/devtool/#devtool
devtool: isDebug ? 'cheap-module-inline-source-map' : false
}
//
// Configuration for the client-side bundle (client.js)
// -----------------------------------------------------------------------------
const clientConfig = {
...config,
name: 'client',
target: 'web',
entry: {
client: ['babel-polyfill', './src/client.js']
},
plugins: [
// Define free variables
// https://webpack.js.org/plugins/define-plugin/
new webpack.DefinePlugin({
'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
'process.env.BROWSER': true,
__DEV__: isDebug,
}),
// Emit a file with assets paths
// https://github.com/sporto/assets-webpack-plugin#options
new AssetsPlugin({
path: path.resolve(__dirname, '../build'),
filename: 'assets.json',
prettyPrint: true,
}),
// Move modules that occur in multiple entry chunks to a new entry chunk (the commons chunk).
// https://webpack.js.org/plugins/commons-chunk-plugin/
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: module => {
return module.resource && /node_modules/.test(module.resource)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'client',
async: 'chunk-vendor',
children: true,
minChunks: (module, count) => {
return count >= 3
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
minChunks: Infinity
}),
...(isDebug
? []
: [
// Decrease script evaluation time
// https://github.com/webpack/webpack/blob/master/examples/scope-hoisting/README.md
new webpack.optimize.ModuleConcatenationPlugin(),
// Minimize all JavaScript output of chunks
// https://github.com/mishoo/UglifyJS2#compressor-options
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: {
screw_ie8: true, // React doesn't support IE8
warnings: isVerbose,
unused: true,
dead_code: true
},
mangle: {
screw_ie8: true
},
output: {
comments: false,
screw_ie8: true
}
})
]),
// Webpack Bundle Analyzer
// https://github.com/th0r/webpack-bundle-analyzer
...(isAnalyze ? [new BundleAnalyzerPlugin()] : [])
],
// Some libraries import Node modules but don't use them in the browser.
// Tell Webpack to provide empty mocks for them so importing them works.
// https://webpack.js.org/configuration/node/
// https://github.com/webpack/node-libs-browser/tree/master/mock
node: {
fs: 'empty',
net: 'empty',
tls: 'empty'
}
}
//
// Configuration for the server-side bundle (server.js)
// -----------------------------------------------------------------------------
const serverConfig = {
...config,
name: 'server',
target: 'node',
entry: {
server: ['babel-polyfill', './src/server.js'],
},
output: {
...config.output,
path: path.resolve(__dirname, '../build'),
filename: '[name].js',
chunkFilename: 'chunks/[name].js',
libraryTarget: 'commonjs2',
},
// Webpack mutates resolve object, so clone it to avoid issues
// https://github.com/webpack/webpack/issues/4817
resolve: {
...config.resolve,
},
module: {
...config.module,
rules: overrideRules(config.module.rules, rule => {
// Override babel-preset-env configuration for Node.js
if (rule.loader === 'babel-loader') {
return {
...rule,
options: {
...rule.options,
presets: rule.options.presets.map(
preset =>
preset[0] !== 'env'
? preset
: [
'env',
{
targets: {
node: pkg.engines.node.match(/(\d+\.?)+/)[0],
},
modules: false,
useBuiltIns: false,
debug: false,
},
]
)
}
};
}
// Override paths to static assets
if (
rule.loader === 'file-loader' ||
rule.loader === 'url-loader' ||
rule.loader === 'svg-url-loader'
) {
return {
...rule,
options: {
...rule.options,
name: `public/assets/${rule.options.name}`,
publicPath: url => url.replace(/^public/, '')
}
}
}
return rule
})
},
externals: [
'./assets.json',
nodeExternals({
whitelist: [reStyle, reImage, /react-toolbox/],
}),
],
plugins: [
// Define free variables
// https://webpack.js.org/plugins/define-plugin/
new webpack.DefinePlugin({
'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
'process.env.BROWSER': false,
__DEV__: isDebug,
}),
// Adds a banner to the top of each generated chunk
// https://webpack.js.org/plugins/banner-plugin/
new webpack.BannerPlugin({
banner: 'require("source-map-support").install();',
raw: true,
entryOnly: false
})
],
// Do not replace node globals with polyfills
// https://webpack.js.org/configuration/node/
node: {
console: false,
global: false,
process: false,
Buffer: false,
__filename: false,
__dirname: false
}
}
function overrideRules(rules, patch) {
return rules.map(ruleToPatch => {
let rule = patch(ruleToPatch);
if (rule.rules) {
rule = { ...rule, rules: overrideRules(rule.rules, patch) };
}
if (rule.oneOf) {
rule = { ...rule, oneOf: overrideRules(rule.oneOf, patch) };
}
return rule;
});
}
module.exports = [clientConfig, serverConfig] |
Woohoo! Thanks a ton @creeperyang! 🎉 I just swapped out my webpack config for yours (as I'm still fairly close to the initial kriasoft one) and Had to make some slight modifications in order to make it work (would be curious if you have thoughts as to why): Commented out this section:
Was getting this error at compile time: Changed these:
into:
Thanks again! |
The answer I was hoping to get from this question is: How does this work when you are providing a component library and you have no control over the client? I can't require my clients to add the specified context code. So what do I do?
|
I'm trying to use React Toolbox alongside isomorphic-style-loader in my project. React-Toolbox uses CSS modules for its components.
My current solution that I haven't implemented yet is to import each component and its styles, apply withStyles, and export the new component. An example file would look like:
Is there a way to apply isomorphic-style-loader's
--- Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/38158821-how-to-use-isomorphic-style-loader-effectively-with-component-libraries?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).withStyles()
across all of a library's components without me having to manually import each file and re-export it?The text was updated successfully, but these errors were encountered: