Here at Industrial Logic, we have been working with React and React-Native extensively in the past few years. We have created a few tools that we found to accelerate our development, built through continuous refactoring of our code.
This small series will introduce these tools with a deep dive into how they work and why we think they are awesome for React and React-Native projects.
This article is about our package media-style-sheet: a drop-and-replace extension to StyleSheets that adds dynamic Media Query functionality to your styles.
StyleSheet is a React-Native abstraction similar to CSS StyleSheets for creating styles for your components. You can create a stylesheet with styles passing an object similar to a CSS Stylesheet.
const styles = StyleSheet.create({
container: {
backgroundColor: 'blue',
},
text: {
fontSize: 22,
fontStyle: 'italic',
},
});
Unlike CSS Stylesheets though, React-Native StyleSheets have to be applied directly to a component via their style prop.
function Hello() {
return (
<View style={styles.container}>
<Text style={styles.text}>Hello</Text>
</View>
);
}
The React-Native stylesheet is pretty simple. It allows you to specify a style with a key. That is about it.
One of the React-Native applications we were working on targeted four devices with different resolutions across iOS and Android. Among those different device configurations, there were two vastly different layouts, the tablet layout and the mobile layout. With the tablet layout, (which we initially started with) we could display more on the screen than the mobile layout. We also had a tendency to use more row-based layouts on the tablet than column-based based on the mobile device.
When we initially started to support the mobile layout, we ended up writing our styles like this:
const styles = StyleSheet.create({
text: {
fontSize: isMobile() ? 18 : 22,
fontStyle: 'italic',
},
});
Adding these device-specific ternaries worked for a time. As the application evolved, we needed to start introducing more specific layouts for each device. We then extended this pattern to other device-specific styles.
const styles = StyleSheet.create({
text: {
fontSize: isIOS() ? 15 : isMobile() ? 18 : 22,
fontStyle: 'italic',
},
});
When we only had isMobile()
, it was simple enough to understand what style would be applied at runtime. However, adding nested device queries like isIOS()
on top of it, led to confusion and difficulties in reading which styles were being applied. And as the device queries are just plain javascript, there was no enforcing of standards meaning these device overrides could be written differently, furthering the burden on the developer reading these stylesheets.
To try and simplify having device-specific styles, our next idea was to make separate style sheets per device and compose the shared styles among them.
const styles = isMobile() ? getMobileStyles() : getTabletStyles();
const sharedStyles = {
fontStyle: 'italic',
};
function getMobileStyles() {
return StyleSheet.create({
text: {
...sharedStyles,
fontSize: 18,
},
});
}
function getTabletStyles() {
return StyleSheet.create({
text: {
...sharedStyles,
fontSize: 22,
},
});
}
This was okay, but it led to an abundance of objects and behavior in what should be a simple stylesheet. We never knew we would miss CSS StyleSheets.
We knew something had to change as none of these options led to clear, easy style sheets for different device layouts. We were missing media-queries from plain CSS. We wanted blocks of styles next to each other so we could tell what styles were shared, and what differed between the device layouts.
In a moment of frustration, one of us wrote a stylesheet we wished we could write.
const styles = StyleSheet.create({
text: {
fontStyle: 'italic',
fontSize: 22,
// typescript error: not a legal key in a StyleSheet
ios: {
fontSize: 15,
},
// typescript error: not a legal key in a StyleSheet
mobile: {
fontSize: 18,
},
},
});
This led to Typescript errors. If only we could write it this way, it would be easier to understand.
This frustration led to MediaStyleSheet
! MediaStyleSheet
flattens the device-specific styles at runtime if it applies to those devices.
As it grew, we finally enhanced it through the magic of complex Typescript. We made it compile, and useful when working with MediaStyleSheets! The best part is, that this is a drop and replace with the existing React-Native StyleSheet.
Here is a working example you can use today with MediaStyleSheet
.
const MediaStyleSheet = createMediaStyleSheet({
ios: isIOS,
mobile: isMobile,
});
const styles = MediaStyleSheet.create({
text: {
fontStyle: 'italic',
fontSize: 22,
ios: {
fontSize: 15,
},
mobile: {
fontSize: 18,
},
},
});
Shared styles are defined at the top level, and additional properties can be added or overridden in each media option.
MediaStyleSheet
performed a very impactful design change for us: it replaced procedural behavior with semantic data. This made all of our stylesheets easier to modify and understand. By having all of the component styles near each other, we were able to see and eliminate duplication across our stylesheets.
In open-sourcing MediaStyleSheet
, we made it so the media options passed in (like tablet, and mobile) are configurable to make it useful in more contexts.
If you want to check out media-style-sheet, give it a try and let us know what you think!