jss icon indicating copy to clipboard operation
jss copied to clipboard

[react-native] React Native Support

Open jtag05 opened this issue 9 years ago • 47 comments

First of all, thank you for the incredible library. It really solves so many fundamental issues I had with embedding CSS in my react projects.

I recently began work on a bit of a test project using react-native to build a universal app and would really like to incorporate JSS. I see that react-native support is on the roadmap, so I was wondering if there has been any attempts at this thus far? If not, I may be so inclined to take a stab at it on a fork.

jtag05 avatar Nov 22 '16 17:11 jtag05

Not yet, also I need to learn react-native first, so can't predict when this may happen. If someone is experienced in react native and can tell what jss needs to addd to support it I am happy to support it of course. Will leave this issue as a reminder in case some one wants to help out.

kof avatar Nov 22 '16 20:11 kof

@kof In the case of ReactNative, the styling is already a JS object configuration written inside a js module. And the styling attributes are sort of camelcase CSS attributes, just like this tool.

I'm attaching here a component I've created which behaves as a searchbar for ReactNative, you can take it as reference to understand how it works: https://github.com/fmsouza/rn-seed-simple/blob/master/src/common/searchbar.js

fmsouza avatar Jan 15 '17 15:01 fmsouza

@fmsouza thanks, yes, the question is what specifically does jss needs to do to have a full RN support. If its just about sharing style declarations, it should work already.

kof avatar Jan 16 '17 09:01 kof

@kof If this solution idea is to make these styles cross-platform between web and mobile, maybe it can be a little more tricky, as you don't have nested styles or events like hover and active to deal with in RN. For RN the output will be mostly a raw js object for each element.

fmsouza avatar Jan 17 '17 00:01 fmsouza

Yeah, cross-platform styles will need to use only subset of css supported by rn.

kof avatar Jan 17 '17 00:01 kof

I think its a matter of just structuring the code, one can put it in separate dirs or use suffixes.

kof avatar Jan 17 '17 00:01 kof

I guess we are not using exact same presentational components for rn and web, right?

kof avatar Jan 17 '17 11:01 kof

Indeed, the fondamental element are different. For example DIV is VIEW in rn.

Here is another CSS in JS project which is compatible with RN. Maybe worth to take a look but I agree that the main effort should be to lint all incompatible rules.

dagatsoin avatar Jan 17 '17 13:01 dagatsoin

Sample from Official Documentation:


const styles = StyleSheet.create({
  bigblue: {
    color: 'blue',
    fontWeight: 'bold',
    fontSize: 30,
  },
  red: {
    color: 'red',
  },
})

class LotsOfStyles extends Component {
  render() {
    return (
      <View>
        <Text style={styles.red}>just red</Text>
        <Text style={styles.bigblue}>just bigblue</Text>
        <Text style={[styles.bigblue, styles.red]}>bigblue, then red</Text>
        <Text style={[styles.red, styles.bigblue]}>red, then bigblue</Text>
      </View>
    )
  }
}

We could use this wrapper:

  • https://github.com/styled-components/css-to-react-native

To get API like:

import { fromCSS } from 'jss-react-native'

const styles = fromCSS`
  bigblue {
    color: blue,
    font-weight: 'bold',
    font-size: 30px
  }

  red {
    color: 'red'
  }
`

class LotsOfStyles extends Component {
  render() {
    return (
      <View>
        <Text style={styles['red']}>just red</Text>
        <Text style={styles['bigblue']}>just bigblue</Text>
        <Text style={[styles['bigblue'], styles['red']]}>bigblue, then red</Text>
        <Text style={[styles['red'], styles['bigblue']]}>red, then bigblue</Text>
      </View>
    )
  }
}

The same approach we could apply for original JSS. I meant fromCSS function by using Template Strings. According to #220.

DenisIzmaylov avatar Feb 18 '17 02:02 DenisIzmaylov

Media queries for react-native https://github.com/tuckerconnelly/uranium

kof avatar May 14 '17 21:05 kof

Not sure if it's the best approach, but we are using the following structure for sharing JSS on React Web and Stylesheets on React Native

//platform.js
export function createStyles(styles, component) {
    return injectSheet(styles, component);
}
//platform.native.js

export function createStyles(styles){
    return WrappedComponent => class extends React.Component {
        render() {

            return (
                <WrappedComponent
                    classes={StyleSheet.create(styles)}
                    {...this.props}
                />
            );
        }
    }
}
//commonComponent.js
import createStyles from "./platform.js"
const styles = {button: {backgroundColor: 'blue'}}

@createStyles(styles)
export class SearchListView extends React.Component {
	render() {
            const {classes} = this.props;
            return <View style={classes.button}/>;
        }
}

grigored avatar May 31 '17 13:05 grigored

Sounds good, what about structure? How do you organize react-native, web and shared styles? Please write a blog post about this stuff!

kof avatar May 31 '17 13:05 kof

@kof you have:

  • button/index.js for web
  • button/index.ios.js for ios
  • button/index.android.js for android

iamstarkov avatar Jun 05 '17 10:06 iamstarkov

We have just began unifying the React and React Native components, will write a blog post/sample github repo once we are ready to deploy in production, as we still have to sort some issues. But basically we are using what @iamstarkov mentioned: we use file.js, file.native.js (if we need separate ios/android files, then file.ios.js and file.android.js also works). Webpack only looks at file.js when bundling our web app, and the react native packager is smart enough to pick the right file.

grigored avatar Jun 05 '17 11:06 grigored

there is also interesting react-primitives project

iamstarkov avatar Jun 05 '17 11:06 iamstarkov

@grigored also interested by this stuff feel free to share more examples. thanks.

canercandan avatar Feb 21 '18 16:02 canercandan

@canercandan tldr: have a look here https://github.com/grigored/cross-platform-react/tree/master/src/primitives/createStyles .

long story: I'm in the very early stage of open sourcing a library to help with writing code once, rendering everywhere (web, native, sketch). Should have an alpha in a week or two. This is based on a project on which my team worked in the past two-three years. The key points:

  1. have most of the material-ui components available on native, with a native Android/IOS look and feel. And have other native components available for web.
  2. be able to write your own styles like this:
const styles = {
  container: {
    ios: {
      padding: 5,
    },
    web: {
      margin: 5
    },
  }
}

or

 const styles = {
  container: {
    padding: {
      ios: 5,
      web: 10,
      sketch: 15
    },
  }
}
  1. be able to reuse navigation/ redux etc across platforms.

grigored avatar Feb 21 '18 18:02 grigored

@grigored look nice thanks for sharing it, I am definitely interested for seeing more about this API

canercandan avatar Feb 21 '18 19:02 canercandan

Just a little feedback folks, coming from styled-components which I love it,

but I learned two things from there.

  1. I wouldn't like to see any string based driven styles, JS objects are fine specially creating tooling around, it is always easier to work with than manipulating strings.

  2. I wouldn't expect any unit added to anything from the package. styled-components assumes that px are the unitless unit for RN and creates more chaos than helping the project and one of the main reason for them is that they were Web developers forcing Mobile developer to their decisions. As a 6+ web developer and less than 1 year on RN, I can't agree on that at all. I used it and I added more chaos to the code without any reason. Each platform has it own unique constrains and it is better to embrace them and improve the quality of the development with the tools, but definitely do not force things on developers.

yordis avatar Mar 27 '18 05:03 yordis

How about the following ideas: (I'm thinking about this from the context of the JSS configuration Material UI uses)

  • On RN create style objects with StyleSheet.create using the style object instead of css classes. Though allow for a factory to be passed in to use instead of StyleSheet.create (in case a wrapper exists)
  • For now, keep a permanent registry of styles that have been registered and use shallowEqual to re-use style objects. StyleSheet.create currently permanently registers object and never releases them, so if JSS is used dynamically like primaryButton: { color: theme.palette.primary } and when the theme changes (primary color blue, then green, then back to blue) we create styles for {color: 'blue'}, then {color: 'green'}, then {color: 'blue'} the first two objects will still exist in memory. And if this is done continually (flipping between two colors for some reason) we will leak memory. So we should at least re-use styles when they haven't changed. (This issue could be solved if work on this and this is resumed)
  • (Or we could just skip this issue for now by just passing inline style objects to be used in style instead of registering stylesheets)
  • Pass the StyleSheet.create ids instead of classes. Though maybe we should pass styles instead of classes. Hard to say, there are pros and cons.
  • Ignore css selectors and media queries. By default just discard them on native (let them be used for doing things like creating hover styles on the desktop which are irrelevant in RN). But keep the metadata around, in case someone wants to write plugins that do things with them on native. For example someone might want to turn {foo: {color: 'red', '&:active': {color: 'blue'}}} into styles = {foo: {color: 'red'}, fooActive: {'color': 'blue'}} then write something like web:className={classes.foo} native:style={this.state.isPressed ? styles.fooActive : styles.foo}.
  • However as a special case if the css query matches /^\&+$/ then merge the styles from that into the main object.
    • i.e. '&' which is just explicitly the class on it's own and sometimes used like foo: {'&': {...mainStyles}, 'span': {...labelStyles}} to keep things on the same level instead of writing foo: {...mainStyles, 'span': {...labelStyles}} and '&&', '&&&', etc which are used to make selectors like .foo-1.foo-1 that have a higher specificity and override other styles.
    • In RN, there is no selector specificity, you just give an array of styles and the later styles override the styles earlier in the array. So by merging styles in native we can write styles with '&&' that override others on the web and on native just put them at the end of the array and use the same style object code (instead of having to explicitly copy them to the root for native and use '&&' on web for specificity).
  • Write an optional plugin that will warn you about coding mistakes that work on web but will break React Native. For example, when {width: '24px'} is written emit a warning that it should be written as {width: 24} instead so it'll work in both React DOM and React Native. This will help avoid having people write a bunch of code for the web and have it work fine, then switch over to the native version of their app and find it's breaking and they need to refactor a bunch of things like '24px' to 24 just to see that the actual code they wrote works fine in React Native.
    • We could also write an eslint plugin.
  • Optionally we could also write a plugin that would make it easier to write platform-specific styles:
    withStyles({
      foo: {
        backgroundColor: 'red',
        web: {
          // We use a h2 on the web, so reset a few things on web
          margin: 0,
          padding: 0,
          fontSize: 'inherit',
          color: 'inherit',
        },
        native: {
          aspectRatio: 16/9,
        },
        android: {
          elevation: 1,
        },
        ios: {
          ...shadowStylesInsteadOfElevation,
        },
      }
    })
    
    Or perhaps more web-like without overloading the selector pattern and forcing us to hard code a platform list (others exist like windows/uwp and hopefully people will create more platform implementation):
    withStyles({
      foo: {
        backgroundColor: 'red',
        '@media (platform: web)': {
          ...
        },
        '@media (platform: native)': {
          ...
        },
        '@media (platform: android)': {
          ...
        },
        '@media (platform: ios)': {
          ...
        },
      }
    })
    
    Or if we don't want to come up with special cased non-standard css and instead want to go the JS route using Symbols or special prefixes:
    withStyles({
      foo: {
        backgroundColor: 'red',
        [JSSPlatform('web')]: {
          ...
        },
        [JSSPlatform('native')]: {
          ...
        },
        [JSSPlatform('android')]: {
          ...
        },
        [JSSPlatform('ios')]: {
          ...
        },
      }
    })
    
    Where JSSPlatform is a function (which could be given a better name) that just accepts a platform name (or 'web' or 'native') and outputs a Symbol (or just prefixes the string with a special internal prefix).

dantman avatar Mar 27 '18 23:03 dantman

I love this

withStyles({
  foo: {
    backgroundColor: 'red',
    [JSSPlatform('web')]: {
      ...
    },
    [JSSPlatform('native')]: {
      ...
    },
    [JSSPlatform('android')]: {
      ...
    },
    [JSSPlatform('ios')]: {
      ...
    },
  }
})

Yes it could be more noice but it is really clear what is going on. Also

withStyles({
  foo: {
    backgroundColor: 'red',
    ...JSSPlatform({
      // insert platform key here
      web: {},
      ios: {}
    })
  }
})

yordis avatar Mar 27 '18 23:03 yordis

@yordis your last code block just made me remember this is actually 90% taken care of.

withStyles({
  foo: {
    backgroundColor: 'red',
    ...Platform.select({
      web: {},
      ios: {}
    })
  }
})

We just need to wrap react-native's Platform module with one that exposes a browser/web version and accepts the native key to refer to all react-native platforms (it'll have to be up to react-native-web what it wants to consider itself, depending on how it handles jss like web styles). Heck, someone has been squatting on the react-platform package name all this time, we could easily take that package name over and provide a standard-ish react package for universal platform code splitting of React.

If we're going to go that route, we might as well also implement a babel transform that will see Platform.select and strip out certain keys. It can be set to a web preset that will strip out everything except web, a native preset that will strip out web, and any other string will be presumed to be a specific native platform (even if it's something like 'windows' which isn't part of the standard react-native) and will strip out any thing except native and a platform key matching the string given. Note that this may need extra though for the electron platform. Maybe the transform should just accept either an includes or an excludes array. (includes: ['web'], excludes: ['web'], includes: ['native', 'android'], includes: ['web', 'electron'])

dantman avatar Mar 28 '18 15:03 dantman

Write an optional plugin that will warn you about coding mistakes that work on web but will break React Native. For example, when {width: '24px'} is written emit a warning that it should be written as {width: 24} instead so it'll work in both React DOM and React Native. This will help avoid having people write a bunch of code for the web and have it work fine, then switch over to the native version of their app and find it's breaking and they need to refactor a bunch of things like '24px' to 24 just to see that the actual code they wrote works fine in React Native. We could also write an eslint plugin.

Maybe I'll change my suggestion on this. Like how JSS acts as an enhancement for browser CSS (camel cased named, automatic prefixing, nesting selectors and media queries, etc...) perhaps we should actually think of it also as an enhancer for React Native StyleSheets.

  • Automatically transform values like '24px' values to 24
  • Automatically transform shorthands like padding: '0 24px to their longer versions paddingLeft: 24, paddingRight: 24, paddingTop: 0, paddingBottom: 0. Likewise for most other basic css shorthands. Especially the flexbox ones.

This of course won't be perfect, we will still need to emit warnings for things like width: '50%' because RN doesn't support any type of size value except density-independent pixels.

dantman avatar Mar 28 '18 15:03 dantman

I am also wondering if it makes sense to mix cross-platform styles in one rule. How much can mobile styles be similar to the web? Wouldn't it be easier to just maintain separate styles for e.g. by using separate files for web/mobile? Or/and extract common styles for all platforms and just import/merge them. Then you only need something that picks the right file, for e.g. during build step for just a function.

kof avatar Mar 28 '18 16:03 kof

@kof They are billed at about 90% as just a subset of css styles. The 10% being a custom aspectRatio, elevation for Android, maybe some quirks in the flexbox implementation though they've been fixing things, the fact you pass objects/styles/arrays of those to style instead of classes to className and a single object with bad practice inline styles to style, and the fact that specificity is based on order in the styles array instead CSS's rules on cascading.

i.e. If you write styles for RN's StyleSheet.create and then just dump them into JSS they will pretty much just work. In fact instead of using screenshots or emulators RN's own styling documentation now just uses a react-native-web based player to display examples live in your browser.

In fact there is already a react-primitives project that exposes View, Text, and StyleSheet that have the RN api but are used on the web.It does attempt to handle specificity by mocking the RN behaviour by controlling the order of the css classes from the effect of the style prop. And it seems to support selectors. But then you're designing your web code based on React Native's api (which is a problem if your library is web first but also wants to support native apps.

dantman avatar Mar 28 '18 17:03 dantman

90% between web and mobile? It might heavily depend on design. I can imagine some buttons being 90% the same, but layouts, margins and more complex components…

kof avatar Mar 29 '18 10:03 kof

Also depending on wether the same person is working on mobile and web, it might be better to have stronger separation if people are different or even teams are different. If you have one component, mobile developer needs to test on the web for each change. It might be handy though if really 90% of the code are the same and same people work on web and mobile.

kof avatar Mar 29 '18 10:03 kof

So I can see both cases useful:

  1. More separation:

// commonStyles.js
export default {
  button: {
    // common styles
  }
}

// buttonStyles.web.js
import styles from './commonStyles'
export default merge({}, styles, {
  button: {
    // specific web styles
  }
}

Then picking the file based on a platform evtl using some build magic. Separate builds can allow to have separate bundles and not load mobile code on the web and vice-versa.

  1. Less separation
const styles = {
  button: {
    // common styles
    display: 'inline-block',
    web: {
      fontSize: X
    },
    mobile: {
      fontSize: Y
    },
    android: {
      // something specific to android only
    }
  }
}

For the later we would need a JSS plugin which takes the platform specific names and merges/removes them based on a platform. All platforms code would be loaded in that case. A babel plugin could optimize that though.

kof avatar Mar 29 '18 10:03 kof

Also for RN, it could be a different preset for e.g. "jss-react-native-preset" which will only use plugins which make sense there.

kof avatar Mar 29 '18 10:03 kof

As a sample, here are the src/List/ListItem.js styles from Material UI annotated with how they relate to React Native:

  • red lines are only relevant to the browser
  • green lines should work the same in RN and browsers
  • yellow lines are relevant to RN but need a tweak to work in RN (e.g border: '1px ... -> borderWidth: 1, ...) which a react-jss plugin could do automatically
  • grey lines need to be moved to a separate class since you need to apply them to a different element in RN, but the style otherwise works the same and you can use the same 2 css classes in both web and React Native (the yellow textDecoration line also needs this)

https://gistpreview.github.io/?0f6ee2998220b2965e2e0280bccd4852

As you can see the majority of the styles that define what the component looks like could be used with JSS in both the web and React Native. The web-only styles are generally things like :hover, animations, and a few css properties that are only necessary on the web because of the long legacy of things we don't really want anymore.

dantman avatar Apr 01 '18 14:04 dantman