Styling
React Native

Pre-requisites

  1. Use native StyleSheet IDs
     
  2. No unnecessary abstractions
     
  3. Style components based on props
     
  4. Separate styles from views and don't mingle logic
     
  5. Easily customise the style from a calling component
     
  6. Understand the relationships between styles and elements
     
  7. Recognise which props are used for styling and logic

Pre-requisites

 0. Does it actually work?

(addendum)

Libraries to compare

react-native-bem

extended stylesheet

styled components

Does it actually work?

🥇

styled

components

🥇

🥉

extended

stylesheet

bem

Use native StyleSheet IDs

StyleSheet.create({
  foo: {
    margin: 10,
  },
  bar: {
    padding: 20,
  }
})


{
  1: foo,
  2: bar,
}

🥇

styled

components

🥇

🥇

extended

stylesheet

bem

No unnecessary abstractions

EStyleSheet.create({
  $size: 20,
  color: '$textColor',
  '@media (min-width: 350) and (max-width: 500)': {
    text: {
      fontSize: '2rem',
    }
  },
  borderRadius: '0.5 * $size'
});

EStyleSheet.build({
  $textColor: '#0275d8'
});
import { TEXT_COLOR } from '@globals';

const windowSize = Dimensions.get('window');
const size = 20;

StyleSheet.create({
  color: TEXT_COLOR,
  text: windowSize.width >= 350 && windowSize.width <= 500 ? 2 : null,
  borderRadius: 0.5 * size;
});

🤷‍♂️

const StyledView = styled.View`
  background-color: papayawhip;
`;

const StyledText = styled.Text`
  color: palevioletred;
`;

const RotatedBox = styled.View`
  transform: rotate(90deg);
  text-shadow-offset: 10px 5px;
  font-variant: small-caps;
  margin: 5px 7px 2px;
`;

Styled Components

Imagine how you'd write the property in React Native, guess how you'd transfer it to CSS, and you're probably right

Probably?!

🤷‍♂️

🥇

styled

components

extended

stylesheet

bem

Style components
based on props

b = bem(selector, this.props, styles);
class Button extends Component {
  render() {
    const style = getStyle(this.props.scale)

    return (
      <View style={style.button} />
    );
  }
}

const getStyle = (scale = 1) => (
  EStyleSheet.create({
    $scale: scale,
    button: {
      width: 100,
      height: 20,
      marginLeft: 10
    }
  });
);
/*
  A new stylesheet is created
  on every render… unless…
*/


import memoize from 'lodash/memoize';

const getStyle memoize((scale = 1) => (
  EStyleSheet.create({
    $scale: scale,
    button: {
      width: 100,
      height: 20,
      marginLeft: 10
    }
  });
));
const Button = styled.button`
  background: ${props => props.primary ? 'palevioletred' : 'white'};
  color: ${props => props.primary ? 'white' : 'palevioletred'};
`;
  • A styleSheet ID is created for every possible combination
     
  • Styling multiple properties based off a prop requires an interpolation for each or an awkward syntax
     
  • The recommended syntax fails our lint

🥇

styled

components

🥈

🥉

extended

stylesheet

bem

Separate styles from views and don't mingle logic

import styles from './styles';

…

onHideUnderlay = () => { this.setState({ SisPressed: false }); }
onShowUnderlay = () => { this.setState({ SisPressed: true }); }

b = bem(selector, { ...this.props, ...this.state }, styles);

…

<TouchableHighlight
  onHideUnderlay={this.onHideUnderlay}
  onPress={this.onPress}
  onShowUnderlay={this.onShowUnderlay}
>
  <View style={this.b('button')}>
    <Text style={this.b('button__text')}>
      {this.props.text}
    </Text>
  </View>
</TouchableHighlight>
import styles from './styles';

…

onHideUnderlay = () => { this.setState({ isPressed: false }); };
onShowUnderlay = () => { this.setState({ isPressed: true }); };

…

<TouchableHighlight
  onHideUnderlay={this.onHideUnderlay}
  onPress={this.onPress}
  onShowUnderlay={this.onShowUnderlay}
>
  <View style={[
    styles.button, this.state.isPressed && styles.buttonIsPressed
  ]}>
    <Text style={[
      styles.button__text,
      this.state.isPressed && styles.buttonIsPressedButton__text,
    ]}>
      {this.props.text}
    </Text>
  </View>
</TouchableHighlight>
const StyledButton = styled.View`
  align-items: center;
  background-color: ${(props) => (props.pressed ? t('highlight') : t('secondary'))};
  border-color: ${(props) => (props.pressed ? t('highlight') : t('secondary'))};
  border-width: 2;
  flex-direction: row;
  border-radius: 5;
  justify-content: center;
  height: ${Platform.OS === 'ios' ? 48 : 50};
  paddingHorizontal: ${s('small')};
  width: 100%;
`;

const StyledButtonText = styled.Text`
  color: ${(props) => (props.pressed ? c('gray 222222') : c('white'))};
  font-size: 15;
  letter-spacing: 1.9;
  text-align: center;
  width: 100%;
`;

…

<TouchableHighlight
  onHideUnderlay={this.onHideUnderlay}
  onPress={this.onPress}
  onShowUnderlay={this.onShowUnderlay}
>
  <StyledView pressed={this.state.isPressed}>
    <StyledText pressed={this.state.isPressed}>
      {this.props.text}
    </StyledText>
  </View>
</TouchableHighlight>
const StyledPressedButton = StyledButton.extend`
  background-color: ${t('highlight')};
  border-color: ${t('highlight')};
`;

const StyledPressedButtonText = StyledButtonText.extend`
  color: ${c('gray 222222')};
`;

…

render() {
  const ButtonComponent = this.state.isPressed ?
    StyledPressedButton : StyledButton;
  const ButtonTextComponent = this.state.isPressed ?
    StyledPressedButtonText : StyledButtonText;

  return (
    <TouchableHighlight
      onHideUnderlay={this.onHideUnderlay}
      onPress={this.onPress}
      onShowUnderlay={this.onShowUnderlay}
    >
      <ButtonComponent>
        <ButtonTextComponent>
          {this.props.text}
        </ButtonTextComponent>
      </ButtonComponent>
    </TouchableHighlight>
  );
}

🥇

styled

components

🥈

🥉

extended

stylesheet

bem

Easily customise the style from a calling component

class Button extends Component {
  render() {
    return (
      <TouchableHighlight>
        <View>
          <Text>
            {this.props.text}
          </Text>
        </View>
      </TouchableHighlight>
    );
  }
}

class App extends Component {
  render() {
    return (
      <Button style={{ margin: 20 }} />
    );
  }
}
class Button extends Component {
  render() {
    return (
      <TouchableHighlight>
        <View style={this.b('button')}>
          <Text>
            {this.props.text}
          </Text>
        </View>
      </TouchableHighlight>
    );
  }
}

class App extends Component {
  render() {
    return (
      <Button style={{ margin: 20 }} />
    );
  }
}
class Button extends Component {
  render() {
    return (
      <TouchableHighlight>
        <View style={this.props.style}>
          <Text>
            {this.props.text}
          </Text>
        </View>
      </TouchableHighlight>
    );
  }
}

class App extends Component {
  render() {
    return (
      <Button style={{ margin: 20 }} />
    );
  }
}
class Button extends Component {
  render() {
    return (
      <TouchableHighlight>
        <View style={[
          styles.button,
          this.state.isPressed && styles.buttonIsPressed,
          this.props.style,
        ]}>
          <Text>
            {this.props.text}
          </Text>
        </View>
      </TouchableHighlight>
    );
  }
}

class App extends Component {
  render() {
    return (
      <Button style={{ margin: 20 }} />
    );
  }
}
class Button extends Component {
  render() {
    return (
      <TouchableHighlight>
        <StyledButton style={this.props.style}>
          <StyledButtonText>
            {this.props.text}
          </StyledButtonText>
        </StyledButton>
      </TouchableHighlight>
    );
  }
}

class App extends Component {
  render() {
    return (
      <Button style={{ margin: 20 }} />
    );
  }
}

🥇

styled

components

🥈

🥉

extended

stylesheet

bem

Understand the relationships between styles and elements

team-list {}
team-list--under-a-certain-condition {}
team-list.in-a-particular-state {}

  team-list__title {}
  team-list--under-a-certain-condition team-list__title {}
  team-list.in-a-particular-state team-list__title {}

  team-list-player {}
    team-list-player__name {}
    team-list-player__position {}
<StyledTeamList />
<StyledTeamList underACertainCondition />
<StyledTeamList inAParticularState />

  <StyledTeamListTitle />
  <StyledTeamListTitle underACertainCondition />
  <StyledTeamListTitle inAParticularState />

  <StyledTeamListPlayer />
    <StyledTeamListPlayerName />
    <StyledTeamListPlayerPosition />

🥇

styled

components

🥈

🥉

extended

stylesheet

bem

Recognise which props are used for styling and logic

<Button Mfoo="bar" Sbaz={true} />

🤷‍♂️

Props = {
  MbgColor: string // eslint-disable-line no-unused-props
}

🤢

{props.MbgColor === 'NRLAccount' ? 'INSIDE PASS' : props.text}
b = bem(selector, {
  MbgColor: this.props.userAccess,
}, styles);

🥇

styled

components

🥈

🥉

extended

stylesheet

bem

🥇

styled

components

🥈

🥉

extended

stylesheet

bem

7

0

1

2

3

2

2

2

3

More Bem bad parts

  1. You have to add it (and rename it) on every component
     
  2. It has to check every prop to see if it's a suitable bem prop
     
  3. Child blocks and child block children don't respond to modifiers and states applied to the parent
     
  4. Due to the naive way the style prop is applied, child blocks also apply it
     
  5. Developers who prefer to annotate the base, nested, parent > child relationships (eg. 'foo bar') in the style name couldn't.

Introducing Vogue

All of the good bits, none of the bad

import vogue from 'react-native-vogue';

class Button extends Component {
  render() {
    return (
      <TouchableHighlight
        onHideUnderlay={this.onHideUnderlay}
        onPress={this.onPress}
        onShowUnderlay={this.onShowUnderlay}
      >
        <View style={this.props.v('root')}>
          <StyledText
            style={this.props.v('text')}
            weight="600"
          >
            {this.props.text}
          </StyledText>
        </View>
      </TouchableHighlight>
    );
  }
}

export default vogue(Button, styles);
export default vogue(Button, styles, (props) => ({
  '--on-light': props.onLight,
  '--size': props.size,
  '--type': props.type,
  '.is-pressed': false,
}));
// Used in the component
type Props = {
  text: string,
  url: string,
  onPress: () => mixed,
};

// Used to affect styles
type VogueProps = {
  onLight: boolean,
  size: string,
  type: string,
};

class Button extends Component<Props> {
  …
}

export default vogue(Button, styles, (props: Props & VogueProps) => ({
  '--on-light': props.onLight,
  '--size': props.size,
  '--type': props.type,
  '.is-pressed': false,
}));
class Button extends Component<Props> {
  onHideUnderlay = () => {
    this.props.inVogue({ '.is-pressed': false });
  }

  onShowUnderlay = () => {
    this.props.inVogue({ '.is-pressed': true });
  }

  render() {
    return (
      <TouchableHighlight
        onHideUnderlay={this.onHideUnderlay}
        onShowUnderlay={this.onShowUnderlay}
      />
    );
  } 
}
export default StyleSheet.create({
  'root': {
    …
  },
  '--type-hollow': {
    …
  },
  '.is-pressed': {
    …
  },
  'text': {
    …
  },
  '--on-light text': {
    …
  },
  '--type-hollow text': {
    …
  },
  '--size-14 text': {
    …
  },
  '.is-pressed text': {
    …
  },
});
type Props = {
  text: string,
  uppercase: boolean,
  userAccess: string,
};

type VogueProps = {
  banner: boolean,
  bgColor: string,
  fontSize: string,
};

export default vogue(TopicTag, styles, (props: Props & VogueProps) => ({
  '--banner': props.banner,
  '--bg-color': props.bgColor || props.userAccess,
  '--font-size': props.fontSize,
}));
<TopicTag MbgColor={props.userAccess} />

…

{props.MbgColor === 'NRLAccount' ? 'INSIDE PASS' :
  props.uppercase ? props.text.toUpperCase() : props.text
}
<TopicTag userAccess={props.userAccess} />

…

{props.userAccess === 'NRLAccount' ? 'INSIDE PASS' :
  props.uppercase ? props.text.toUpperCase() : props.text
}

WDYT?

Styling React Native

By Matt Stow

Styling React Native

  • 1,703