Jss: [react-jss] Without a HOC

Created on 3 Aug 2018  路  31Comments  路  Source: cssinjs/jss

_From @kof on February 7, 2016 18:10_

As an alternative approach to current, we can render sheets as a child element.

const sheet = jss.createStyleSheet(style)

render() {
  return (
    <div>
      <Jss sheet={sheet} />
    </div>
  )
}

Ref counting can be achieved by using a WeakMap.

const map = new WeakMap()

class Jss {
  componentWillMount() {
    const {sheet} = this.props
    const counter = map.get(sheet) || 0
    if (!counter) sheet.attach()
    map.set(sheet, counter + 1)
  }

  componentWillUnmount()聽{
    const {sheet} = this.props
    const counter = map.get(sheet) - 1
    if (counter) {
      map.set(sheet, counter)
    } else {
      sheet.detach()
      map.delete(sheet)
    }
  }

  render() {
    return null
  }
}

_Copied from original issue: cssinjs/react-jss#31_

high idea

Most helpful comment

_From @kof on July 13, 2017 18:6_

Another idea:

const styles = {
  button: {
    color: 'red'
  }
}

const StyledButton = () => (
      <Jss styles={styles}>
        {({classes}) => (
          <button className={classes.button}>My Button</button>
        )}
      </Jss>
)

All 31 comments

_From @AlastairTaft on May 10, 2016 14:21_

Would this help with server rendering?

I'm looking for some sort of solution similar to how react-look works where I can wrap all my markup in a <LookRoot /> tag and then call a StyleSheet.renderToString() method to get all my styles.

Just trying to figure out if this is something react-jss would/should support or if I should have a go at writing my own package that depends on react-jss?

_From @kof on May 10, 2016 14:26_

This issue has nothing to do with SSR. This is about having refs counting and lazy evaluation features react-jss provides without using a HOC and though sheet can be defined at render time.

_From @AlastairTaft on May 10, 2016 14:33_

Ah, thanks for clearing that up. Not completely following on the HOC though. On the documentation on the home page it says

You can use it as a higher-order component to inject JSS.

Does this mean react-jss does support a higher order component to grab all the styles, or is this something it will never support, is there an alternate way of achieving the same thing?

Apologies if these are naive questions.

_From @kof on May 10, 2016 14:37_

react-jss does exactly what is in readme, you can get css for all sheets directly from jss https://github.com/jsstyles/jss/blob/master/docs/js-api.md#a-style-sheets-registry
react-jss is only using jss.

_From @AlastairTaft on May 10, 2016 15:0_

Thanks for your help, that's exactly what I was looking for. I'll be sure to give that a try now.

_From @moliver-bb on May 10, 2016 16:23_

this is a really nice solution! we can create common JSS components for general styles and add them where needed, including multiple sheets per component!

_From @umidbekkarimov on May 19, 2016 8:42_

I think it can be easily done with react-side-effect (will start some experiments)

I was looking for solution where I can use context (again integration with material-ui) and this is the thing!

_From @umidbekkarimov on May 19, 2016 10:17_

Done,

I used plain arrays with lodash utils instead of WeakMaps

import fp from 'lodash/fp';
import withSideEffect from 'react-side-effect';

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

const {
    node,
    object
} = PropTypes;

const mapUniqueSheets = fp.flow(fp.map('sheet'), fp.uniq);
const attachSheets = fp.forEach(sheet => sheet.attach());
const detachUnusedSheets = fp.flow(fp.difference, fp.forEach(sheet => sheet.detach()));

let prevSheets;

@withSideEffect(mapUniqueSheets, sheets => {
    if (prevSheets) {
        detachUnusedSheets(prevSheets, sheets);
    }

    attachSheets(sheets);

    prevSheets = sheets;
})
export default class Jss extends Component {
    static propTypes = {
        sheet: object,
        children: node
    }

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

Usage:

import jss from 'jss';

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

import Jss from '../../components/jss/Jss';

const {
    bool,
    func,
    object
} = PropTypes;

export default class RecoverPassword extends Component { 
    static contextTypes = {
        muiTheme: object.isRequired
    }

    constructor(props, context) {
        super(props, context);

        const {
            muiTheme: {
                palette: {
                    accent1Color
                }
            }
        } = context;

        this.sheet = jss.createStyleSheet({
            root: {
                backgroundColor: accent1Color
            }
        });
    }

    render() {
        const {
            classes
        } = this.sheet;

        return (
            <Jss sheet={this.sheet}>
                 <div className={classes.root}>
                        Yo ho ho!
                 </div>
            </Jss>
        );
    }
}

_From @kof on May 19, 2016 10:23_

Looks nice @umidbekkarimov !

_From @kof on May 19, 2016 10:26_

You won't be able to reuse the same sheet across component instances in case you create sheet in constructor? However this might be what you want.

_From @umidbekkarimov on May 19, 2016 10:48_

Looks nice @umidbekkarimov !

Thank you! Will you add this component to current react-jss package? I would be happy to provide test cases but I'm too busy now

You won't be able to reuse the same sheet across component instances in case you create sheet in constructor? However this might be what you want.

Yes it's just my use case where I need an access to this.context

_From @umidbekkarimov on May 19, 2016 12:54_

Main problem here is - hot reloading, and it's solvable by moving jss.createStyleSheet from constructor to componentWillUpdate for NODE_ENV=development but adding this condition to every component is not a solution.

_From @kof on May 19, 2016 13:0_

an option is to extend a Jss class instead of Component and do the magic there.

_From @umidbekkarimov on May 19, 2016 13:20_

In this case if i want to use componentWillUpdate in my component i also have to call super. componentWillUpdate() to make magic work.

So currently HOC is the best option but no props/context will be there.

I'm thinking about

@useSheet((props, context) => ({
      root: {
          color: props.color || context.myTheme.color
      }
}))
export default class Foo extends Component {}

_From @kof on May 19, 2016 13:26_

Yeah, OOP becomes ugly quickly. How would you implement such a HOC?

_From @umidbekkarimov on May 19, 2016 13:56_

  1. Decorator (useSheet or new one) will receive mapPropsAndContextToRules function and options (like contextTypes or jss settings)
  2. In componentWillMount and componentWillReceiveProps we will call this.setState(mapPropsAndContextToRules(this.props, this.context))
  3. In componentWillUpdate compare previous rules with current and if there any difference - create new sheets
  4. As it's done in connect from react-redux we can look to mapPropsAndContextToRules.length to define optimisation steps
function useSheet(mapPropsAndContextToRules, {
    pure: true,
    contextTypes,
    ...restOptions
} = {}) {
    const updateOnPropsChanges = mapPropsAndContextToRules.length >= 1;
    const updateOnContextChange = mapPropsAndContextToRules.length == 2  

    return Target => class WithSheet extends Component {
         static contextTypes = updateOnContextChange ? contextTypes : null;

         componentWillMount() {
              const nextState = mapPropsAndContextToRules(this.props, this.context);
              this.sheet = jss.createStyleSheet(nextState);
              this.setState(nextState);
         }

         componentWillReceiveProps(nextProps) {
             if (updateOnPropsChanges && !isEqual(this.props, nextProps)) {
                 this.setState(mapPropsAndContextToRules(nextProps, this.context));
             }
         }

         shouldComponentUpdate(nextProps, nextState) {
             return updateOnContextChange || !isEqual(this.state, nextState) || !isEqual(this.props, nextProps);
         }

         componentWillUpdate(nextProps, nextState) {
             if (!isEqual(this.state, nextState)) {
                 this.sheet = jss.createStyleSheet(nextState);
             }
         }
    };
}

_From @kof on May 19, 2016 14:9_

sounds like a plan, we need to cover current react-jss with tests before integrating it though, because we will break it otherwise.

_From @b2whats on June 27, 2016 20:57_

my implementation

import React, { Component, PropTypes } from 'react'
import jss from 'jss'
import hoistNonReactStatics from 'hoist-non-react-statics'

let jssInstance = jss

const isObject = obj => Object(obj) === obj

function decorate(WrappedComponent, rules, options) {
  const sheet = jssInstance.createStyleSheet({}, options)
  const instances = new Set()
  const registredRules = new Set()
  const themes = new WeakMap()

  function generateRules(theme, defaultRules) {
    const globalRules = {}
    const componentRules = {}
    const mergedRules = { ...defaultRules, ...theme }

    Object.keys(mergedRules).forEach(selector => {
      const rule = mergedRules[selector]

      if (typeof rule === 'string') {
        globalRules[selector] = rule
        return false
      }

      if (isObject(rule)) {
        const createdRule = jssInstance.createRule(selector, rule) // this
        const { className } = createdRule
        componentRules[selector] = className

        if (registredRules.has(className)) return false

        registredRules.add(className)
        sheet.addRule(selector, rule) // overhead!!! we do the same above
      }
    })

    themes.set(theme, { ...componentRules, ...globalRules })
  }

  function mount(instance, theme = rules) {
    instances.add(instance)

    !themes.has(theme) && generateRules(theme, rules)
    if (instances.size === 1) sheet.attach()
  }

  function unmount(instance) {
    instances.delete(instance)

    if (instances.size === 0) sheet.detach()
  }

  class StyleSheetWrapper extends Component {
    componentWillMount() {
      mount(this, this.props.theme)
    }

    componentWillUnmount() {
      unmount(this)
    }

    render() {
      return <WrappedComponent {...this.props} sheet={themes.get(this.props.theme || rules)}/>
    }
  }

  const displayName = WrappedComponent.displayName
                   || WrappedComponent.name
                   || 'Component'

  StyleSheetWrapper.propTypes = {
    theme: PropTypes.object,
  }
  StyleSheetWrapper.WrappedComponent = WrappedComponent
  StyleSheetWrapper.displayName = `JSS(${displayName})`

  return hoistNonReactStatics(StyleSheetWrapper, WrappedComponent)
}

export default function useSheet(rules, options) {
  if (rules instanceof jss.constructor) {
    jssInstance = rules

    return useSheet
  }

  return (WrappedComponent) => decorate(WrappedComponent, rules, options)
}

Usage:

**files: Button.js**

function Button({ color = 'default', sheet, children, ...otherProps }) {
  return (
    <button className={sheet.button, sheet[color]} {...otherProps}>
      {children}
    </button>
  )
}

const styles = {
  button: {
    fontSize: '2em',
  },
  default: {
    color: 'red',
  },
  green: {
    color: 'green',
  },
}

export default useSheet(styles)(Button)

**files: any.js**


const Buttons = () => (
  <div>
   // sheet - {button:{width: '100px'}, default: {same}, green: {same}}
    <Button theme={test}>Global</Button>
   // sheet - {button:{same}, default: {color: 'black'}, green: {same}}
    <Button theme={test2}>Object</Button> 
   // sheet - all styles default 
    <Button>Default</Button>
  </div>
)

let global = jss.createStyleSheet({
  '.global-hash-selector': {
    width: '100px',
  }
}, {named: false}).attach()
// we can also use css modules
// const styles from './styles.css'

const test = {
  button: 'global-hash-selector',
  // button: styles.button
}

const test2 = {
  default: {
    color: 'black',
  },
}

_From @b2whats on June 28, 2016 13:30_

@kof what do you say?

_From @kof on June 28, 2016 13:32_

Need to find time to go through all this, useful would be a list of features/fixes we are implementing here in natural language. Implementation details doesn't matter that much.

_From @b2whats on June 28, 2016 14:0_

@kof what do you mean -

useful would be a list of features/fixes we are implementing here in natural language

_From @kof on June 28, 2016 14:1_

a list of features/fixes we are talking about written down in english.

_From @b2whats on June 28, 2016 14:3_

my implementation bad work well in current implementation jss issue

_From @b2whats on June 28, 2016 15:11_

@umidbekkarimov how can I contact you ? skype or gitter ?

_From @iamstarkov on July 13, 2017 7:18_

I鈥檓 struggling to see the use case for this feature

_From @kof on July 13, 2017 18:4_

There is a sentiment that HOC's are not always a good fit, because they hide methods.

_From @kof on July 13, 2017 18:6_

Another idea:

const styles = {
  button: {
    color: 'red'
  }
}

const StyledButton = () => (
      <Jss styles={styles}>
        {({classes}) => (
          <button className={classes.button}>My Button</button>
        )}
      </Jss>
)

I think we should close this one in favor of #907, wdyt?

The only problem is that you won't be able to use it with classes

What do you mean?

Ah you mean hooks won't work with classes? yes, I think we should be fine with this, since we are all moving towards this.

Was this page helpful?
0 / 5 - 0 ratings