The Native Primitives

Lesson 1 of 5

There Is No div

"Design depends largely on constraints." — Charles Eames, Design Q&A (1972)

Part I was about the runtime. This Part finally writes some interface, and the first thing to notice is how little vocabulary you get.

HTML has around a hundred elements and, more importantly, one completely generic box. A <div> means nothing, contains anything, and can be made to look like anything — which is why the web's component libraries are built almost entirely out of them. React Native has no such element. View looks like the equivalent and is not: measured on 0.81.4, it cannot contain text, and the renderer enforces that with an error rather than a warning.

That single constraint reorganises how you build interfaces, and it is not an oversight. The rest of the primitive set is similarly narrow and similarly deliberate, and this chapter is about what each one actually is — traced through the source, because two of them turn out to render different native components depending on where you put them, which has no equivalent in HTML at all.

This is also the first chapter where the rn try playground is genuinely honest, so the blocks below are real. Where it diverges from a device, I say so at the point it matters rather than in a footnote.

The big question

On the web, a <span> is a span wherever you put it. In React Native, the same <Text> element resolves to one native component at the top level and a different one when nested inside another Text. What problem is that solving, and what would break if it did not?

A smaller vocabulary, on purpose

React Native's index.js exposes about seventy exports, of which roughly two dozen are components. The ones you will use constantly are far fewer than that, and each resolves to a specific native class rather than to a generic container.

Traced through the 0.81.4 source, the mapping is literal:

you write host component on the device
View RCTView a UIView / Android View
Text RCTText a text-rendering view
Image RCTImageView an image view
ScrollView RCTScrollView a native scroll view
TextInput RCTSinglelineTextInputView / RCTMultilineTextInputView on iOS, AndroidTextInput on Android a platform text field

That last row is worth a pause. TextInput does not resolve to one component even within a single platform — single-line and multi-line are different native views on iOS — and the Android name is not merely a different string, it is a different implementation with different behaviour. Part III is largely about consequences of that kind.

React — Try it Yourself
Loading editor…
Preview

Notice what is doing the work. Every box is a View, every piece of text is wrapped in a Text, and the chip is a View containing a Text rather than a styled text element. That nesting is not a stylistic preference — it is the only legal arrangement, and the next section is why.

Everyday analogy

A generic shipping container will take anything: furniture, grain, machinery, other containers. Its usefulness is precisely that it makes no assumptions, and the cost is that nothing about it helps you — the crane operator, the customs form and the insurer all need to be told separately what is inside.

Now consider a system with a tanker, a flatbed, a refrigerated unit and a grain hopper. You have lost the ability to put anything anywhere, and in exchange every part of the system knows more: the loader knows how to fill a hopper, the temperature is somebody's responsibility by construction, and "this is refrigerated" never needs to be written on a form. React Native's primitives are the second system. Text is not a box that happens to hold letters; it is a text-layout container, and the framework can rely on that.

View cannot contain text

This is the rule that catches everyone coming from the web, and it is enforced rather than advised. The error string lives in both renderers — ReactFabric-dev.js and ReactNativeRenderer-dev.js — and reads:

"Text strings must be rendered within a <Text> component."

So <View>Hello</View> is not a slightly unidiomatic way to write it. On a device it fails.

The reason is layout. A View is laid out by Yoga as a flexbox box: it has a size, children with sizes, and no concept of lines, baselines, word breaking or font metrics. Text needs all of those, and they belong to a text engine rather than a box engine. Rather than making every view carry a dormant text layout system, React Native gives text its own container and requires you to use it.

One playground divergence to state here, because it matters for the block below: react-native-web warns where a device errors. I tested it — rendering a bare string inside a View logs "A text node cannot be a child of a <View>" and then renders it anyway. So the playground will let you do something a device rejects, and this is exactly the sort of place to trust the rule over the demo.

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

The practical consequence is a habit rather than a burden: every string in a React Native app is wrapped, and after a day you stop noticing. What you gain is that any component receiving children knows whether it is dealing with text or with boxes, without inspecting them.

Where this came from

The generic <div> is a historical accident that turned out to be load-bearing. HTML's early elements were semantic — paragraphs, headings, lists — and div arrived in HTML 3.2 as a generic *division* with no meaning at all, intended for grouping. CSS then made it possible to give any element any appearance, and the combination produced the modern web's dominant building block: an element that means nothing, contains anything, and looks like whatever you say.

React Native was designed after that history rather than during it, and chose the opposite. Its primitives correspond to things the platforms actually have — a view, a text container, an image view, a scroll view — because the framework's job is to drive those, not to invent an abstraction above them. That is why the set is small and why it maps so cleanly: it is not a design language, it is the intersection of what UIKit and the Android view system already agree on.

Text renders a different component when nested

Here is the finding that most changed how I think about these primitives, and it is not visible from the documentation.

Reading Text.js, there is a React context called TextAncestorContext, and the component checks it:

const hasTextAncestor = useContext(TextAncestorContext);
if (hasTextAncestor) {
  // renders NativeVirtualText

And in TextNativeComponent.js, those two are genuinely different host components:

  • NativeText → uiViewClassName: 'RCTText'
  • NativeVirtualText → uiViewClassName: 'RCTVirtualText'

So a top-level <Text> becomes a real text view, and a <Text> nested inside another becomes a virtual text span — not a view at all, but a run of styled text inside the parent's layout. That is why nested text inherits font properties, participates in the parent's line breaking, and wraps mid-sentence, while a nested View does none of those things.

There is no HTML equivalent. A <span> is a span wherever it appears; its behaviour changes but its identity does not. Here the identity changes.

React — Try it Yourself
Loading editor…
Preview

Look at the first paragraph as it renders: the bold and coloured runs sit inside the same flowing text, breaking across lines wherever the line break falls. That is the virtual-text behaviour, and react-native-web mirrors it structurally — the outer Text becomes a div and the nested ones become span elements carrying a textHasAncestor class, which is a faithful analogue of RCTText against RCTVirtualText.

The engineer's notebook

The mental model worth adopting is that Text is a layout mode, not a box. Once you are inside a Text, you are in a text-layout world where children are runs of characters, styles cascade, and the unit of arrangement is the line. Outside it you are in a flexbox world where children are rectangles and nothing cascades. Most confusion about why some style "does not work" is really a confusion about which of those two worlds the element is in.

The practical corollary is that styling a container will not style the text inside it. Putting fontSize on a View does nothing, because a View has no font — text properties belong on a Text, and they only inherit from a Text to a nested Text. Coming from CSS, where font-size on a wrapper cascades to everything beneath it, this is the single most common early surprise, and it follows exactly from the two-worlds rule rather than being an arbitrary restriction.

Image is a view, not a document element

The same "it is a platform view" rule produces a constraint that catches people even faster than the text rule, because it fails silently.

An <img> on the web sizes itself. You give it a src, the browser fetches the bytes, reads the intrinsic dimensions, and reflows the page around them. That works because layout can happen after the image arrives.

React Native cannot do that. Layout runs on the shadow tree, on its own queue, computed before and independently of any network fetch — so an Image with a remote source and no dimensions has nothing to be sized from. I checked what renders: without a width and height it produces a container with a flex basis and no size at all, so it collapses and shows nothing. Add dimensions and they appear.

That is why Image carries static methods a document element would never need. Image.getSize and Image.prefetch both exist on 0.81.4 — the first because you sometimes genuinely must know an image's dimensions before you can lay it out, and the second because deciding layout before the bytes arrive is a worse experience if the bytes then arrive slowly.

React — Try it Yourself
Loading editor…
Preview

The first frame is empty, and that is the entire lesson. Nothing errored, no warning appeared, and the image loaded perfectly well — it simply had no space to occupy. "My image is not showing" is almost always this, and it is a layout question rather than a loading one.

The exception is a bundled image, imported through the asset system rather than fetched: those have known dimensions at build time, so they size themselves. That asymmetry — local images size themselves, remote ones cannot — is worth holding, because it means the bug appears when you switch a placeholder for a real URL.

Context decides what an element becomes

Once you have seen it with Text, the pattern turns up elsewhere, and it is worth naming as a general property: in React Native, the same JSX element can resolve to different native components depending on where it sits.

The clearest second example is in the source alongside the text components: RCTTextInlineImage. An <Image> placed inside a <Text> does not become the ordinary RCTImageView — it becomes an inline image participating in text layout, so it can sit on a line with a baseline like a character rather than as a block.

And TextInput resolves by platform and configuration rather than by nesting: RCTSinglelineTextInputView or RCTMultilineTextInputView on iOS, AndroidTextInput on Android.

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

That is the underlying reason for a rule you will otherwise memorise: text-related styles inherit from Text to Text and stop at any View. It is not a cascade rule with an exception. It is that the nested elements are part of one text layout, and a View starts a new box.

In the real world

The most common early bug is setting text styles on a container. Someone writes <View style={{ fontSize: 18, color: "#333" }}> around several Text children, sees nothing change, and concludes styles are broken. They are not: a View has no font, and there is no cascade from a box to the text inside it. The fix is either putting the style on each Text or wrapping in a Text so the nesting actually inherits.

The second is a conditional that renders a bare string. Code like {count && (count + " items")} inside a View is fine while count is zero — the expression is falsy and nothing renders — and throws the moment it is not, because a string appeared where a Text was required. It fails on real data rather than in development, which is the worst possible timing, and it is why the truthiness idiom is more dangerous here than on the web.

The third is trusting a web-shaped mental model of nesting. Wrapping Text in a View to group it is right; wrapping runs of text in a View to style them is not, because the moment you cross a View boundary you leave text layout and the runs stop flowing as one paragraph. The visual result — text that suddenly stacks instead of wrapping — looks like a flexbox bug and is really the two-worlds rule.

What the constraint buys

It is worth ending on the trade, because a smaller vocabulary is only a good deal if it returns something.

Every element maps to a real platform view. There is no generic box that the framework has to interpret, which is why View can be a thin wrapper with no drawing code, as Part I found.

The layout engine can be simpler. Yoga only ever lays out boxes. Text layout is delegated to the platform's text engine, which already knows about fonts, scripts, ligatures and line breaking in every language the OS supports — none of which Yoga has to model.

Accessibility gets better defaults. A Text is a text node to the screen reader by construction, because it is a text view on the device.

And the cost is real: you write more wrapping, you cannot style text from a container, and you must know which of the two worlds you are in.

The item worth arguing about is the second one, because delegating text layout is why React Native renders Arabic, Devanagari and emoji correctly without shipping a shaping engine of its own. You inherit whatever the operating system does, which is decades of work nobody on your team had to think about.

That last point is the strongest argument for the whole design, and it is invisible until you need it. Text rendering in the world's writing systems is enormously difficult, and React Native's answer is to not do it — which is only available because Text is a distinct primitive that maps to the platform's own text view.

Common Mistakes

  1. Treating View as a div. It cannot contain text, and both renderers throw "Text strings must be rendered within a <Text> component."

  2. Setting text styles on a container. A View has no font, and there is no cascade from a box to the text inside it.

  3. Rendering a bare string from a conditional. {count && \${count} items`}` is fine while falsy and throws on real data, which is the worst timing.

  4. Assuming nested Text is like a nested span. It resolves to RCTVirtualText rather than RCTText — a different native component, not a styled variant.

  5. Wrapping runs of text in a View to style them. Crossing a View leaves text layout, so the runs stop flowing as one paragraph.

  6. Expecting an Image in a Text to behave like one outside it. It becomes RCTTextInlineImage and sits on the line with a baseline.

  7. Assuming TextInput is one component. iOS has separate single-line and multi-line views, and Android has its own implementation entirely.

  8. Learning the text rule from the playground. react-native-web only warns and renders anyway; a device errors.

  9. Reaching for a generic container that does not exist. There is no element that means nothing — the primitive you pick declares what it is.

  10. A remote Image with no width and height. Layout runs before the bytes arrive, so it collapses silently — nothing errors and nothing shows.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Warm-up
Why can't you put a string directly inside a View?
Model answer — Because a View is a box, not a text container. It is laid out by Yoga as flexbox — it has a size and children with sizes, and no concept of lines, baselines, word breaking or font metrics. Text needs all of those, so React Native gives text its own primitive and requires you to use it; the renderer enforces this with the error "Text strings must be rendered within a <Text> component." The practical consequence is that every string in a React Native app is wrapped in a Text, and a related surprise is that putting fontSize on a View does nothing, because a box has no font and there is no cascade from a box to the text inside it.
Intermediate
What is different about a Text nested inside another Text?
Model answer — It is a different native component. Reading the source, Text checks a TextAncestorContext, and if it is inside another Text it renders NativeVirtualText rather than NativeText — which resolve to RCTVirtualText and RCTText respectively. So the outer one is a real text view and the nested one is a virtual run of styled text inside the parent's layout. That is why nested text inherits font properties, participates in the parent's line breaking and wraps mid-sentence, while a nested View becomes a box sitting on the line instead. There is no HTML equivalent: a span is a span wherever you put it, its behaviour changes with context but its identity does not.
Advanced
React Native has a deliberately small primitive set. What does that buy, and what does it cost?
Model answer — It buys a direct mapping to platform views, which is why View can be a thin wrapper with no drawing code at all, and it keeps the layout engine's scope to boxes — Yoga never has to model lines, baselines or scripts, because text layout is delegated to the platform's own text engine. That delegation is the strongest part of the design and the least visible: React Native renders Arabic, Devanagari and emoji correctly without shipping a shaping engine, because Text maps to a real platform text view. Accessibility also gets correct defaults by construction rather than by annotation. The costs are genuine: you wrap every string, you cannot style text from a container, and you have to know whether an element is in box layout or text layout, which is the source of most early confusion. I would frame it as choosing specific primitives over one generic box, and the generic box is what the web has — enormously flexible, and it means nothing on its own.
Company lens — how the same topic gets asked
micro1Practical: rebuild a small card layout with the right primitives, and explain why the text is wrapped the way it is.
GooglePushes on the mechanism: what nested Text resolves to, why layout is split between Yoga and a text engine, and what that delegation buys.
MetaIts own framework — expect the design rationale for a small primitive set and how it maps onto UIKit and the Android view system.
AmazonAccessibility-flavoured: what a screen reader sees, why the primitives give better defaults, and where you still have to annotate.

Hands-on

Hands-on

Build the rules this chapter measured. Implement validateTree so it finds every bare string not inside a Text, reporting all of them rather than the first; resolve so an element name plus its context yields the right host component, including the nesting and platform cases; and inheritedTextStyle so text styles cascade from Text to nested Text and stop dead at a View. The checks print a scorecard — the stubs deliberately fail, so expect a low score before you start.

JavaScript — Try it Yourself
Loading editor…
Console
Runs automatically. Edit the code to see it change.

Summary

  • React Native has no generic box. View is not a div: it cannot contain text, and the renderer enforces that with an error.
  • Each primitive maps to a specific native class — RCTView, RCTText, RCTImageView, RCTScrollView — traced through the 0.81.4 source.
  • TextInput is not one component: iOS has separate single-line and multi-line views, and Android has its own implementation.
  • Text resolves differently when nested. A TextAncestorContext switches it between RCTText and RCTVirtualText, so nested text is a run rather than a view.
  • That is why nested text flows and inherits while a nested View becomes a box sitting on the line — they are different kinds of thing on the device.
  • An Image inside a Text becomes RCTTextInlineImage, so context can change an element's identity, which has no HTML equivalent.
  • Text is a layout mode, not a box. Inside it, children are runs and styles cascade; outside it, children are rectangles and nothing cascades.
  • Text styles on a View do nothing, which is the most common early surprise and follows directly from the two-worlds rule.
  • The playground warns where a device errors on bare text in a View, so this is a rule to trust rather than a behaviour to explore.
  • Delegating text layout to the platform is what the small primitive set buys: scripts, shaping and emoji work without React Native implementing any of it.

In the next chapter, Text Is Not a Box, we stay inside that second world — what the text engine actually gives you, why numberOfLines is not a CSS property, how fonts behave when the platform owns them, and the alignment rules that stop making sense the moment you expect a box.

Editorial reflection

The TextAncestorContext discovery is the kind of thing that makes reading source worthwhile. I knew nested Text behaved differently — everyone who has written React Native knows that — and I assumed it was a styling rule with special handling. It is not: the component checks a context and renders a different host component, RCTVirtualText instead of RCTText. Once you know that, a whole cluster of remembered behaviours collapses into one mechanism, which is exactly the trade this Library keeps making: a few minutes of source reading against a list of memorised exceptions.

What I find genuinely elegant is the text-engine delegation, and it is almost never mentioned in discussions of React Native's design. Text rendering across the world's writing systems — bidirectional text, Indic shaping, emoji sequences, per-language line breaking — represents an amount of work that dwarfs most of the framework, and React Native's answer is to not do any of it. That is only possible because Text is a distinct primitive mapping to a real platform text view. The constraint people notice as "why do I have to wrap every string" is the same decision as "why does Arabic just work".

Where I would push back on my own framing is the tidiness of the two-worlds model. It is a good teaching device and it leaks: a View inside a Text is neither cleanly a box nor cleanly a run, and its behaviour is more platform-dependent than the tidy version admits. I have kept the model because it explains the great majority of cases with one idea, and flagged the seam rather than pretending the boundary is clean — a model that never surprises you is usually a model that has stopped being checked.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.