View and Text
View is the native container, Text is the only place a string may live, and Text is the one component whose styles reach children.
<View style={styles.box}><Text numberOfLines={2}>Label</Text></View>View and Text are the two components every screen is built from. A View is a real native container: UIView on iOS, a ViewGroup on Android. A Text is a native text node. Almost everything else in the framework is these two plus styling.
A View is the nearest thing to a div, and it is not one. Its children are laid out with flexbox, and flexDirection defaults to column, not row. It takes no font styles, and nothing it sets reaches its children. There is no cascade and no inheritance.
Strings belong to Text
A string cannot sit directly inside a View. It throws at runtime: Text strings must be rendered within a <Text> component. A number counts as a string here.
Text is the single exception to the inheritance rule. Inside a Text, layout stops being flexbox and becomes text layout: children wrap at the end of a line rather than sitting in boxes, and a nested Text keeps the parent's font styles and adds its own. React Native flattens that tree into one attributed string per platform.
Two ways to write one style
style={[styles.body, styles.strong]} is the short form. It merges left to right, later entries winning, so it is the same result as writing the merge yourself with style={{...styles.body, ...styles.strong}}. Falsy entries in the array are ignored, which is why you see [styles.base, isActive && styles.active] everywhere. Both forms are equally valid and you need to recognise each.
Example
import { StyleSheet, Text, View } from "react-native";
type CardProps = {
title: string;
body: string;
author: string;
};
// A card: a View for the frame, Text for every string inside it.
export default function Card({ title, body, author }: CardProps) {
return (
<View style={styles.card}>
<Text style={styles.title} numberOfLines={2}>
{title}
</Text>
{/* The inner Text keeps styles.body and adds styles.author on top.
This is the only place in React Native where a style reaches a
child element. A View would pass nothing down. */}
<Text style={styles.body}>
{body} <Text style={styles.author}>{author}</Text>
</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
padding: 16,
borderRadius: 8,
backgroundColor: "#ffffff",
borderWidth: 1,
borderColor: "#e4e4e7",
},
title: { fontSize: 18, fontWeight: "600", marginBottom: 8 },
body: { fontSize: 15, lineHeight: 22, color: "#3f3f46" },
author: { fontWeight: "600", color: "#18181b" },
});
// On screen: a white rounded card. The title sits on top in bold, clipped
// with an ellipsis if it runs past two lines. The body follows, with the
// author's name in bold at the end of the same paragraph, wrapping as part
// of the sentence rather than starting a new block.When to use it
- Build a card or list row: a View for the frame, Text for each string.
- Bold or colour a single word inside a sentence without splitting the string into pieces.
- Cut a long headline to one or two lines with numberOfLines and ellipsizeMode.
- Track down the "Text strings must be rendered within a <Text> component" crash after a conditional render.
More examples
The bare string error, and the zero trap
On screen: a red pill with the number in it, then the label beside it; when count is 0 only the label shows. The commented lines are the two ways this component is usually written wrong. The second is the one that reaches production, because it works for every count except zero.
import { StyleSheet, Text, View } from "react-native";
type BadgeProps = {
label: string;
count: number;
};
export default function Badge({ label, count }: BadgeProps) {
return (
<View style={styles.badge}>
{/* Both of these throw "Text strings must be rendered within a
<Text> component":
<View>{count}</View>
{count && <Text>{count}</Text>} // renders 0 when count is 0
An explicit comparison returns null instead of a number. */}
{count > 0 ? <Text style={styles.count}>{count}</Text> : null}
<Text style={styles.label}>{label}</Text>
</View>
);
}
const styles = StyleSheet.create({
badge: {
flexDirection: "row", // default is column, so a row must be asked for
alignItems: "center",
gap: 6,
},
count: {
minWidth: 20,
paddingHorizontal: 6,
borderRadius: 10,
overflow: "hidden",
textAlign: "center",
color: "#ffffff",
backgroundColor: "#dc2626",
},
label: { fontSize: 15 },
});Truncating, and making one word tappable
On screen: a one-line headline cut off with an ellipsis, a two-line teaser below it, and a blue "Read more" that calls onOpen when tapped. Reach for Text onPress for an inline link inside a sentence; for a real button use Pressable, which gives you feedback and a hit target.
import { StyleSheet, Text, View } from "react-native";
type ArticleRowProps = {
headline: string;
teaser: string;
onOpen: () => void;
};
export default function ArticleRow({ headline, teaser, onOpen }: ArticleRowProps) {
return (
<View style={styles.row}>
{/* numberOfLines truncates after layout, so wrapping is accounted for.
ellipsizeMode picks where the dots go: head, middle, tail or clip.
On Android only "tail" behaves correctly above one line. */}
<Text style={styles.headline} numberOfLines={1} ellipsizeMode="tail">
{headline}
</Text>
<Text style={styles.teaser} numberOfLines={2}>
{teaser}
</Text>
{/* Text has its own onPress. Useful for an inline link. On iOS a grey
oval highlights the text on press down by default, which is what
suppressHighlighting turns off; Android gets no such highlight.
Either way the touch target is only the glyphs themselves. */}
<Text style={styles.action} onPress={onOpen} suppressHighlighting={false}>
Read more
</Text>
</View>
);
}
const styles = StyleSheet.create({
row: { paddingVertical: 12, gap: 4 },
headline: { fontSize: 17, fontWeight: "600" },
teaser: { fontSize: 14, color: "#52525b" },
action: { fontSize: 14, fontWeight: "600", color: "#1d4ed8" },
});A View passes nothing down to its children
On screen: four lines of the same size, the last two greyed out. It shows the two consequences of no inheritance: font styles must be repeated on every Text or kept in a shared style object, and the array and spread forms of style are interchangeable.
import { StyleSheet, Text, View } from "react-native";
export default function SettingsHeader() {
return (
<View style={styles.panel}>
{/* Adding fontSize: 20 to styles.panel would change nothing here, and
TypeScript rejects it outright: a View style is a ViewStyle, which
has no font properties. There is no inherited font size. */}
<Text style={styles.heading}>Settings</Text>
<Text style={styles.heading}>Account</Text>
{/* Two ways to share one look. The array merges left to right. */}
<Text style={[styles.heading, styles.muted]}>Advanced</Text>
{/* Identical result, written as one merged object: */}
<Text style={{ ...styles.heading, ...styles.muted }}>Beta</Text>
</View>
);
}
const styles = StyleSheet.create({
panel: { padding: 16, gap: 8, backgroundColor: "#fafafa" },
heading: { fontSize: 20, fontWeight: "600", color: "#18181b" },
muted: { color: "#71717a", fontWeight: "400" },
});
Discussion