/jsxstyle

Primary LanguageJavaScript

jsxstyle

The problem

The problem with vjeux's solution

  • The problem with StyleSheet.create() is you're not colocating your style with where it's used for no reason. If you're instantiating a component that's only intended to be styled (which I've found is the majority of my DOM nodes!), the style properties should be on that component's props. Stylesheets shouldn't be used for reuse; instead the ideal API is to build inline styles w/ JavaScript, using components for reuse, and the power of JS expressions to compute styles (and share constants) where needed.
  • Performance of the inline styles technique is questionable. Inline styles means more bytes down the wire for server rendering and more GC pressure client-side. At the very least, there hasn't been a lot of sweat put into "inline styles for everything" by browser vendors yet.
  • It also makes the web inspector harder to use since you can't update all instances of a component on the page simultaneously.

The solution

Provide a nice inline style API

var {Block, Flex, InlineBlock, rgb} = require('jsxstyle');
var Theme = require('./MyTheme');

var hovered = false; // This could be dynamic

var avatar = (
  <Flex
    width={Theme.GRID_UNIT * 10}
    marginLeft="auto"
    marginRight="auto"
    alignItems="center">
    <img src="..." />
    <Block marginLeft={Theme.GRID_UNIT} color={Theme.primaryColor} background={hovered && rgb(255, 0, 0)}>
      <InlineBlock fontWeight="bold">Username here</InlineBlock>
      subtitle here
    </Block>
  </Flex>
);

Look at how nice this is. You don't have to jump from stylesheet to code to make changes. It's also very clear exactly what's going on, and very predictable since you aren't relying on cascade as much. You can also wrap much of this styling up into a helper component, so users only see <MediaBlock image="..."><EmphText>Username here</EmphText>subtitle here</MediaBlock>.

Another nice thing is you can lint this, hell, if you have a powerful enough type system you can type check it, minify it, dead-code-eliminate it, whatever. This does for CSS what React did for HTML.

Convert this nice API to traditional React with inline styles

You can use a straightforward AST transform script to convert the previous example to:

var avatar = (
  <div style={{
    display: 'flex',
    width: Theme.GRID_UNIT * 10,
    marginLeft: 'auto',
    marginRight: 'auto',
    alignItems: 'center'
  }}>
    <img src="..." />
    <div style={{
      marginLeft: Theme.GRID_UNIT,
      color: Theme.primaryColor,
      background: hovered && rgb(255, 0, 0)
    }}>
      <span style={{fontWeight: 'bold'}}>Username here</span>
      subtitle here
    </div>
  </div>
);

This is just a straightforward transformation into the React of today. You can also do this at runtime too if you want (both are included in the repo)

Extract the literal CSS into a stylesheet

The problem with the above example is React has to do a lot of extra work diffing a bunch of style properties that we know won't ever change. Since some of the parameters are expressions magic Babel optimizations won't be able to help either.

Also if you want to edit some of these properties in the web inspector, it won't edit all instances of the component.

We can remedy this by pulling the literal CSS (i.e. the constant string and integer properties) into a stylesheet with autogenerated classnames. This is often the majority of your CSS.

var avatar = (
  <div style={{width: Theme.GRID_UNIT * 10}} className="c1">
    <img src="..." />
    <div style={{
      marginLeft: Theme.GRID_UNIT,
      color: Theme.primaryColor,
      background: hovered && rgb(255, 0, 0)
    }}>
      <span className="c2">Username here</span>
      subtitle here
    </div>
  </div>
);
.c1 {
  display: flex;
  margin-left: auto;
  margin-right: auto;
  align-items: center;
}

.c2 {
  font-weight: bold;
}

Tell the static analyzer about your constants

The previous step helped, but there's a lot of inline styles left since we have some expressions in the style object. What I found building with this technique is that most expressions are simple arithmetic and string concats on constants that are known at build time. If we can tell our static analyzer about the Theme object, it can substitute the grid unit, color palette, and typography at build time, evaluate the expression, and move it into the stylesheet.

var avatar = (
  <div className="c1">
    <img src="..." />
    <div style={{background: hovered && rgb(255, 0, 0)}} className="c3">
      <span className="c2">Username here</span>
      subtitle here
    </div>
  </div>
);
.c1 {
  display: flex;
  margin-left: auto;
  margin-right: auto;
  align-items: center;
  width: 100px; /* from Theme.js evaluated at build time */
}

.c2 {
  font-weight: bold;
}

.c3 {
  margin-left: 10px; /* from Theme.js evaluated at build time */
  color: red; /* from Theme.js evaluated at build time */
}

Make the generated CSS nicer

Since this is built on AST transformation, we can make the CSS pretty nice and more debuggable than traditional techinques:

.example_js__1 {
  /* example.js:2 */
  display: flex;
  margin-left: auto;
  margin-right: auto;
  align-items: center;
  width: 100px;
}

.example_js__2 {
  /* example.js:5 */
  font-weight: bold;
}

.example_js__3 {
  /* example.js:4 */
  margin-left: 10px;
  color: red;
}

Optimize generated CSS

Since every component has exactly one unique class name corresponding to its lexical position and does not rely on cascade, you're free to run optimization on the generated CSS. For example, if you have a few different CSS classes that happen to have the same CSS properties, simply throw out all but one, and rewrite the references to the classes you threw out to the one you kept.

To be fair, this isn't a property of using inline styles, but a property of a system that treats CSS like the hostile render target it is and abstracts it away from you.

I don't have an optimizer built, but the primitive jsxstyle/renameClass is included which can be used by an optimizer.

Make style reuse easy

A lot of times you'll want simple reusable components that implement your theme. You can use currying to achieve this.

var {Inline, curry} = require('jsxstyle');

var StandardText = curry(Inline, {color: 'gray', fontSize: 12});
var EmphText = curry(StandardText, {fontWeight: 'bold'});


React.render(<StandardText>Hello world</StandardText>, document.body);

Conclusion

I've built some stuff with this at this point and I never want to go back to the stylesheet programming model. I think this model is flexible enough that we can control the implementation tradeoffs (we can use inline styles, fully static stylesheets, or a mix).

Try it

Check out the example/ directory for a bad example. Be sure to inspect the DOM, particularly <head>! In webpack.config.js you can swap out the webpack loader and it will magically fall back to inline styles for everything instead of extracting out a static stylesheet.

What's cool is the webpack loader is an optimization -- since this is just JavaScript, you can use Browserify or Require.js or whatever you want and the app will still work. And since it's pure JS you can package it in npm without hassle.

Open areas of work

  • The reference webpack loader is hacky, exposes internal paths, is not optimized for production, and doesn't work with extract-text-plugin.
  • Media queries / pseudoclasses: <Block width={128} width-iphone={32}>...</Block>?
  • Accidentally making a property dynamic is too easy right now. Maybe require explicitly opting into dynamism with a sentinel: <Block background={jsxstyle.dynamic(hovered ? ... : ...)}>?

FAQ

This isn't semantic

Who cares? Search engines generally don't, and semantic tags don't get you accessibility for free. Instead of making our style components less expressive, we need better tooling for accessibility instead.

Additionally, how many times have you put a <div> on the page just to build a layout or add a border? That's what these components are replacing, not nodes that should contain meaningful content for accessibility (i.e. use unstyled <article>s with a CSS reset).

What about repeated styles?

You should create styled components and reuse those rather than reuse class names. You'll end up with more (tiny) React components like <PrimaryText> and <SecondaryText> and <StackLayout>, but your product engineers will end up writing very little CSS. CSS classes aren't that great for reuse anyway, since they aren't encapsulated and can't specify behavior

What about re-theming off-the-shelf components?

This is a bug in CSS, not a feature. With CSS, any stylesheet can change any part of any component, even if it's a private implementation detail. With jsxstyle, you should make the customizable characteristcs part of the component's public API. This may mean passing component classes into reusable components via props. If this starts to get cumbersome, guess what, this is just dependency injection, which is a very well-understood problem and there's lots of tools to help.

I should probably have a good example of a DI solution that works here, but I don't yet (sorry!). You could probably do something super questionable with component classes on this.context, but you didn't hear it from me.

Is this in production?

I've built a moderately sized app with it used by customers, but it's not at huge scale yet.