Skip to content
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

Add support for client-side batching to generate fewer <style> tags. #1

Merged
merged 1 commit into from
Oct 27, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,55 @@ Support for colocating your styles with your React component.
}
});

# Buffering

To avoid making a new style tag for each individual call to `css`, you can use
buffering. A similar technique will enable server rendering.

On the client, instead of just this:

ReactDOM.render(<App/>, document.getElementById('root'));

You can do this:

StyleSheet.startBuffering();
ReactDOM.render(<App/>,
document.getElementById('root'),
StyleSheet.flush);

Note that this is an optimization, and the first option will work just fine.

Once implemented, server rendering will look roughly like this:

StyleSheet.clearClassNameCache();
StyleSheet.startBuffering();

// Contains the markup with references to generated class names
var html = ReactDOMServer.renderToString(<App/>);

// Contains the CSS referenced by the html string above, and the references
// to the classNames generated during the rendering of html above.
var {styleContents, classNames} = StyleSheet.collect();

return `
<html>
<head>
<style>{styleContents}</style>
</head>
<body>
<div id='root'>{html}</div>
<script src="./bundle.js"></script>
<script>
StyleSheet.markInjected({classNames});
ReactDOM.render(<App/>, document.getElementById('root'));
</script>
</body>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<script>ReactDOM.render(<App />, document.querySelector("#root"))</script>?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that strictly worse than document.getElementById? Seems like querySelector should just be slower because it needs to the the string parser to figure out to search by id?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like yes: https://jsperf.com/getelementbyid-vs-queryselector
I mostly wanted to mention to add the corresponding client-side ReactDOM.render call.

</html>
`;

# TODO

- Examples in the repo
- Autoprefixing
- Batch styles in a single render cycle into a single style tag
- Serverside rendering
- Optional AST transformation to replace StyleSheet.create with an object
literal.
Expand All @@ -80,6 +124,7 @@ Support for colocating your styles with your React component.
the unit is. See
[CSSProperty.js](https://github.com/facebook/react/blob/master/src/renderers/dom/shared/CSSProperty.js)
in React.
- Consider removing !important from everything.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 having everything be !important is pretty annoying with dynamic styles, because if you want a plain style={...} style to overwrite an aphrodite style then you need to append " !important" to it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I too have felt this pain.


# Other solutions

Expand Down
72 changes: 46 additions & 26 deletions dist/aphrodite.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ module.exports =
head.appendChild(style);
};

var classNameAlreadyInjected = {};
var injectionBuffer = "";
var injectionMode = 'IMMEDIATE';

var StyleSheet = {
create: function create(sheetDefinition) {
return (0, _util.mapObj)(sheetDefinition, function (_ref) {
Expand All @@ -97,40 +101,56 @@ module.exports =
_definition: val
}];
});
},

startBuffering: function startBuffering() {
injectionMode = 'BUFFER';
},

flush: function flush() {
if (injectionMode !== 'BUFFER') {
return;
}
if (injectionBuffer.length > 0) {
injectStyles(injectionBuffer);
}
injectionMode = 'IMMEDIATE';
injectionBuffer = "";
}
};

var css = (function () {
var classNameAlreadyInjected = {};
return function () {
for (var _len = arguments.length, styleDefinitions = Array(_len), _key = 0; _key < _len; _key++) {
styleDefinitions[_key] = arguments[_key];
}
var css = function css() {
for (var _len = arguments.length, styleDefinitions = Array(_len), _key = 0; _key < _len; _key++) {
styleDefinitions[_key] = arguments[_key];
}

// Filter out falsy values from the input, to allow for
// `css(a, test && c)`
var validDefinitions = styleDefinitions.filter(function (def) {
return def;
});
// Filter out falsy values from the input, to allow for
// `css(a, test && c)`
var validDefinitions = styleDefinitions.filter(function (def) {
return def;
});

// Break if there aren't any valid styles.
if (validDefinitions.length === 0) {
return "";
}
// Break if there aren't any valid styles.
if (validDefinitions.length === 0) {
return "";
}

var className = validDefinitions.map(function (s) {
return s._name;
}).join("-o_O-");
if (!classNameAlreadyInjected[className]) {
var generated = (0, _generate.generateCSS)('.' + className, validDefinitions.map(function (d) {
return d._definition;
}));
var className = validDefinitions.map(function (s) {
return s._name;
}).join("-o_O-");
if (!classNameAlreadyInjected[className]) {
var generated = (0, _generate.generateCSS)('.' + className, validDefinitions.map(function (d) {
return d._definition;
}));
if (injectionMode === 'BUFFER') {
injectionBuffer += generated;
} else {
injectStyles(generated);
classNameAlreadyInjected[className] = true;
}
return className;
};
})();
classNameAlreadyInjected[className] = true;
}
return className;
};

exports['default'] = {
StyleSheet: StyleSheet,
Expand Down
3 changes: 3 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Examples of using inline-styles-that-work

Run: `npm install && npm run examples`, then open `http://localhost:4114`.
10 changes: 10 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="root"></div>
<script src="./bundle.js"></script>
</body>
</html>
16 changes: 16 additions & 0 deletions examples/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"scripts": {
"examples": "webpack-dev-server --content-base . --port 4114"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel": "^5.8.23",
"babel-core": "^5.8.25",
"babel-loader": "^5.3.2",
"react": "^0.14.0",
"react-dom": "^0.14.0",
"webpack": "^1.12.2",
"webpack-dev-server": "^1.12.0"
}
}
124 changes: 124 additions & 0 deletions examples/src/examples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import ReactDOM from 'react-dom';
import React, { Component } from 'react';
import { StyleSheet, css } from '../../src/index.js';

const StyleTester = React.createClass({
getInitialState: function() {
return {
timer: true,
};
},

componentDidMount: function() {
const flipTimer = () => {
this.setState({
timer: !this.state.timer,
});
setTimeout(flipTimer, 1000);
};

setTimeout(flipTimer, 1000);
},

render: function() {
const testCases = [
<span className={css(styles.red)}>This should be red</span>,
<span className={css(styles.hover)}>This should turn red on hover</span>,
<span className={css(styles.small)}>This should turn red when the browser is less than 600px width</span>,
<span className={css(styles.red, styles.blue)}>This should be blue</span>,
<span className={css(styles.blue, styles.red)}>This should be red</span>,
<span className={css(styles.hover, styles.blue)}>This should be blue but turn red on hover</span>,
<span className={css(styles.small, styles.blue)}>This should be blue but turn red when less than 600px width</span>,
<span className={css(styles.hover, styles.hoverBlue)}>This should turn blue on hover</span>,
<span className={css(styles.small, styles.evenSmaller)}>This should turn red when less than 600px and blue when less than 400px</span>,
<span className={css(styles.smallAndHover)}>This should be red when small, green when hovered, and blue when both.</span>,
<span className={css(styles.smallAndHover, styles.returnOfSmallAndHover)}>This should be blue when small, red when hovered, and green when both.</span>,
<span className={css(styles.red, styles2.red)}>This should be green.</span>,
<span className={css(this.state.timer ? styles.red : styles.blue)}>This should alternate between red and blue every second.</span>,
<a href="javascript: void 0" className={css(styles.pseudoSelectors)}>This should turn red on hover and ???? (blue or red) on active</a>,
];

return <div>
{testCases.map((testCase, i) => <div key={i}>{testCase}</div>)}
</div>;
},
});


const styles = StyleSheet.create({
red: {
color: "red",
},

blue: {
color: "blue",
},

hover: {
":hover": {
color: "red",
}
},

hoverBlue: {
":hover": {
color: "blue",
}
},

small: {
"@media (max-width: 600px)": {
color: "red",
}
},

evenSmaller: {
"@media (max-width: 400px)": {
color: "blue",
}
},

smallAndHover: {
"@media (max-width: 600px)": {
color: "red",
":hover": {
color: "blue",
}
},
":hover": {
color: "green",
}
},

returnOfSmallAndHover: {
"@media (max-width: 600px)": {
color: "blue",
":hover": {
color: "green",
}
},
":hover": {
color: "red",
}
},

pseudoSelectors: {
":hover": {
color: "red",
},
":active": {
color: "blue",
}
},
});

const styles2 = StyleSheet.create({
red: {
color: "green",
},
});

StyleSheet.startBuffering();
ReactDOM.render(<StyleTester/>,
document.getElementById('root'),
StyleSheet.flush);
14 changes: 14 additions & 0 deletions examples/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
var path = require('path');

module.exports = {
entry: [
'./src/examples'
],
module: {
loaders: [{
test: /\.js$/,
loaders: ['babel'],
exclude: /node_modules/
}]
}
}
Loading