Styling
React Native
Pre-requisites
- Use native StyleSheet IDs
- No unnecessary abstractions
- Style components based on props
- Separate styles from views and don't mingle logic
- Easily customise the style from a calling component
- Understand the relationships between styles and elements
- 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
- You have to add it (and rename it) on every component
- It has to check every prop to see if it's a suitable bem prop
- Child blocks and child block children don't respond to modifiers and states applied to the parent
- Due to the naive way the
style
prop is applied, child blocks also apply it
- 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,957