Article

Speed Up React Native Development with Utility Stylesheets 🚀

Margin 8. Padding 16. Border radius 4. Again.


When I was building React Native interfaces, the irritating part wasn't deciding what these values should be. It was repeatedly giving the same little style objects different names.


A small set of typed utility styles makes that work cheaper. Not a new styling language, and certainly not an excuse to build a CSS framework before lunch.

The original video

Updated 10 September 2026: the text below corrects the original bridge/performance explanation and adds the complete generator. Treat the video as historical context where it differs. The code is an illustrative TypeScript implementation, not a device-tested benchmark.

First, a correction about performance

Creating an inline object during render creates a new JavaScript reference. That can matter when a memoized component compares props by reference. It does

not

mean every identical-looking style necessarily triggers a native repaint.


Current React Native documentation describes

StyleSheet.create()

as an identity function whose main practical benefit is static type checking. The old explanation that it automatically sends styles across the bridge once, returns numeric IDs, and prevents further renders is not an accurate description of the current API.


Use this approach for consistency, autocomplete, and less repetitive code. If you're claiming a rendering speedup, profile it on the React Native version and devices you're shipping.

Start with a deliberately small vocabulary

We'll generate margin and padding utilities for all sides, horizontal/vertical pairs, and individual sides. Border radius gets its own treatment because “horizontal border radius” isn't a React Native property.


Our values are React Native layout units, not CSS pixel strings:


const intervals = [0, 2, 4, 6, 8, 12, 16, 24, 32] as const;

The

as const

is important.

const

prevents rebinding the variable, but doesn't by itself turn an array into a readonly tuple of exact numeric literals. The assertion preserves the union

0 | 2 | 4 | ...

, which we can use to construct legal property names.

The complete utility generator

Put this in

utilities.ts

. It uses TypeScript template literal types and runs once when the module is loaded, not during every component render.


import { StyleSheet, type ViewStyle } from 'react-native';
const intervals = [0, 2, 4, 6, 8, 12, 16, 24, 32] as const;
const families = ['margin', 'padding'] as const;
const directions = [
'',
'Horizontal',
'Vertical',
'Top',
'Right',
'Bottom',
'Left',
'Start',
'End',
] as const;
const radiusProperties = [
'borderRadius',
'borderTopLeftRadius',
'borderTopRightRadius',
'borderBottomLeftRadius',
'borderBottomRightRadius',
] as const;
type Interval = (typeof intervals)[number];
type Family = (typeof families)[number];
type Direction = (typeof directions)[number];
type RadiusProperty = (typeof radiusProperties)[number];
type SpacingKey = `${Family}${Direction}${Interval}`;
type RadiusKey = `${RadiusProperty}${Interval}`;
type UtilityKey = SpacingKey | RadiusKey;
type UtilityStyles = Record<UtilityKey, ViewStyle>;
function buildUtilities(): UtilityStyles {
const result: Partial<UtilityStyles> = {};
for (const family of families) {
for (const direction of directions) {
const property = `${family}${direction}` as const;
for (const value of intervals) {
const key: SpacingKey = `${property}${value}`;
const style: ViewStyle = {};
style[property] = value;
result[key] = style;
}
}
}
for (const property of radiusProperties) {
for (const value of intervals) {
const key: RadiusKey = `${property}${value}`;
const style: ViewStyle = {};
style[property] = value;
result[key] = style;
}
}
// The loops above fill every member of UtilityKey.
// Keep the assertion at this construction boundary.
return result as UtilityStyles;
}
export const u = StyleSheet.create(buildUtilities());

The result is intentionally boring:


u.marginTop8; // { marginTop: 8 }
u.padding16; // { padding: 16 }
u.borderRadius4; // { borderRadius: 4 }
u.marginStart12; // Logical spacing for direction-aware layouts.

u.padding7

and

u.borderHorizontalRadius4

are not valid keys. That's the useful bit: the editor suggests the vocabulary we've actually chosen.


The final assertion is necessary because TypeScript doesn't prove that nested loops populate a mapped type completely. It isn't runtime validation. Keep the key union and construction loops together, and test the generated map if you change them.


These are module-generated style objects, not build-time-compiled CSS classes. Calling them “precompiled” without that distinction would promise more than the implementation does.

Use utilities for spacing, names for meaning

import { StyleSheet, Text, View } from 'react-native';
import { u } from './utilities';
export function OrderCard({ delayed }: { delayed: boolean }) {
return (
<View
style={[
styles.card,
u.padding16,
u.marginBottom12,
u.borderRadius8,
delayed && styles.delayed,
]}
>
<Text style={styles.title}>An example order</Text>
<Text style={u.marginTop4}>
{delayed ? 'Running a little late' : 'On its way'}
</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#ffffff',
borderWidth: 1,
borderColor: '#d1d5db',
},
delayed: {
borderColor: '#b45309',
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#111827',
},
});

For the same property, later entries in a style array override earlier ones.

style={[u.padding8, u.padding16]}

ends up with padding 16. Don't rely on broad and directional shorthand combinations without checking the resulting layout.


The utility objects have stable references, but the inline array above is new on every render. If reference equality actually matters at a memoized boundary, hoist a fixed array or memoize a dynamic one. Don't sprinkle memoization everywhere merely because an array exists.


For bidirectional interfaces, prefer

Start

and

End

where you mean leading and trailing space. The physical corner-radius helpers above intentionally describe left and right; extend them with the logical properties supported by your target React Native version if needed.

Check the construction boundary

If you already use Jest with the React Native preset, this small test catches missing combinations and verifies the stored values:


import { StyleSheet } from 'react-native';
import { u } from './utilities';
test('generates the documented utility map', () => {
// 2 spacing families × 9 directions × 9 values
// plus 5 radius properties × 9 values.
expect(Object.keys(u)).toHaveLength(207);
expect(StyleSheet.flatten(u.marginTop8)).toEqual({ marginTop: 8 });
expect(StyleSheet.flatten(u.padding16)).toEqual({ padding: 16 });
expect(StyleSheet.flatten(u.borderRadius4)).toEqual({ borderRadius: 4 });
});

Also run your TypeScript check. A runtime assertion doesn't tell you whether autocomplete rejects a misspelt utility name.

Where I would stop

Spacing and radius are a good fit because their vocabulary is small and predictable. A colour mode, an animated transform, or a responsive layout based on window dimensions needs a different treatment.


Keep dynamic values in component styles or hooks. Use named styles for semantic things such as

selected

,

warning

, and

card

. Don't generate every possible colour, font weight, and layout combination just because the loop is easy to write.


The trade-off is straightforward: every generated utility costs module-initialisation work and memory, including the ones you don't use. A small map is easy to understand. An enormous map is a styling system you now have to maintain.


If a component reads like a shopping receipt of twenty utilities, give it a named style.

Less typing, not more framework

The goal is to stop repeatedly naming

{ marginTop: 8 }

, not to eliminate every custom style in the app. Use a small set of shared values, let TypeScript help you choose them, and spend the saved time on the parts of the interface people actually notice.

Reference

Made with Intenso