React Native

Why My Text Is Going Off Screen? The Truth about React Native Text

Many of us, coding a React Native app, have already been in this situation: « Why on Earth my text refuses to wrap properly? ? »

offScreen1

A React Native text going off screen

What's our first reflex as developer? Go ask Google. And the first result we found is this StackOverflow answer :

offscreen0

  "React native text going off my screen, refusing to wrap. What to do?"

The solution provided is to add a style ++code>{{ flex:1, flexWrap: 'wrap' }}++/code> on the ++code>Text ++/code>element :

offscreen2

StackOverflow first answer to our Problem

It works! ? The text does not overflow out of the screen anymore. Here you can see the before and after:

Capture d'e?cran 2021-09-08 a? 08.58.38

Before and After

But... I did not understand why.

A short edit appears after :

offscreen3

StackOverflow Edit

Again, it's working: ++code>flexShrink++/code> seems to be enough to maintain the text in its container ?.

But more than just fixing my bug, I wanted to understand what was happening. Several explanations came to me, but none I was sure about. And I had even more questions: "Why just flexShrink, what happened to flexGrow? Do I really have to add a flexWrap:wrap before? Should I add "flex:1" on literally every text in my app just to be sure, because it's so efficient ??? And most importantly, is there something else important that I am missing about React Native blocks? Something that could provoque the same bug, in slightly different situations?.."

Capture d'e?cran 2021-09-13 a? 23.15.32

My Brain after fixing the going-out Text
without understanding what I did

You see, it made a lot of reasons to want to solve this mystery. And after some exploration, I happily announce you that I solved it! ? I'll explain everything in this article:

           1. First why is the StackOverflow fix working
           2. Then, more interestingly, I explain why was the Text going off screen in the first place

This article does not require an extended developer experience to be understood. You will need:

        - Basic knowledge of React Native / Javascript
        - Basic knowledge of Flexbox Layout

 

1. Why is the solution working?

1.1 situation

Here is a React Native code that allows to reproduce the bug ?:

++pre>++code>import React, { FunctionComponent } from 'react';
import { Image, StyleSheet, Text, View } from 'react-native';

export const TestScreen: FunctionComponent = () => {
return (
<View>
<View style={styles.titleContainer}><Image style={styles.logo} source={require('./logo.png')} />
<Text>
     {'This is a long title that can really go off the limits!'}.
</Text>
</View>
</View>
)};

const styles = StyleSheet.create({
 titleContainer: {
   flexDirection: 'row',
 },
 logo: {
   width: 100,
 },
})
++/code>++/pre>

Note: You can play around with the code; to obtain the screenshot, I

used some extra padding, colors and radiuses. But they are not impacting for the study: everything essential is above.

offscreen4

Basically, what we have is a++code> Text++/code> in a row container, with an icon before. The row container is itself in a classic vertical ++code>View++/code>. The Text is wrapping, but not enough so we can see it entirely.

1.2 why does the solution works: FlexGrow & FlexShrink

In the Flexbox layout,  ++code>flex: 1++/code>  is a shortcut for 3 style properties:

  • ++code>flexShrink: 1++/code>
  • ++code> flexGrow: 1++/code>
  • ++code> flexBasis: auto++/code>

We won't discuss the  ++code>flexBasis++/code>  property in this article, as it has no impact in the problem. But the two others interests us. 

Note: If you need a reminder about Flexbox properties,
I recommend
this article on CSS-Tricks: clear and well illustrated, made for web CSS but really useful for React Native too. 

I used to believe that it was because of the ++code>flexGrow++/code> part that ++code>flex: 1++/code> worked; we had to allow the element to grow vertically, so the text could gently wrap in a wider space? Nice mental model, but it's wrong!

++code>flexGrow++/code> (and ++code>flexShrink++/code>) controls the size of an item along what is usually called the main axis of the container (see the React Native doc): in our case, horizontally.

Here is a drawing that helped me fix the idea:

FlexGrow & FlexShrink in a Row

Note: I rather use the words "flow" than "main axis" to talk about the direction where the elements align: I find it more illustrative.

This scheme illustrates why ++code>flexShrink: 1++/code> is the only important part: it allows the title to take a smaller width than its initial size.

I hadn't focused on that at first sight, because I was only seeing the height growing...forgetting the width shrinking, when it was actually the part we were controlling!

1.3 HOW did the text went out: Overflow

So the text was just too large at the first place. At least at this point, I understood why it was going off screen: it's the normal ++code>flexWrap : no-wrap++/code> behavior of an element (here, the ++code>title++/code>) inside a parent row view (the ++code>titleContainer++/code>).

When elements are bigger than their parent, they start to overflow them : 

Capture d'e?cran 2021-09-07 a? 21.59.15

Children overflowing in a Row

If the parent has the screen's dimension, elements overflow outside the screen and we don't see them anymore.

We can force them to wrap by using the ++code>flexWrap++/code> property:

Capture d'e?cran 2021-09-07 a? 21.59.17

Children wrapping in a Row

But... why was the initial text too large in the first place? Why did we have to make it shrink?

This is the second question I asked myself, and the answer was trickier than I thought.

 

2. Why were the text too large in the first place?

To answer this question, we "simply" have to answer to the most fundamental question in layout: How the size of an element is computed when it's not explicitly fixed in its style?

2.1 Understanding the auto-sizing of an element: the parent vs content-driven model

I like to think there is 2 ways, and 2 ways only, to understand how an element compute its width and height, when not given:

  • parent-driven: when the element is constrained by its parent, and grows to reach its parent's borders, or shrink to fit in. It is control from above.
  • content-driven: when the element is constrained by its content, and fits the minimal size so what's inside can be seen. Content can be children nodes, or "pure" content (words for a text, pixels for an image...). It is control from beneath.

This is my favorite mental model when talking about Layout. It really allows to understand quickly what goes on screen.

I will apply it to our ++code>title++/code> element, following its two characteristics:

  • First as a ++code>Text++/code> , explaining the auto-sizing of a text works
  • Second as a child in a ++code>View++/code>

2.2 Auto-size of a TEXT

I used to think that a ++code>Text++/code> was like a ++code>View++/code> with ++code>flexDirection: row++/code>, and all of its words being tiny++code> Views++/code> separated by spaces.

In this model, we could have had all the words making a really long line, without wrapping at all. The first StackOverflow answer implicitly refers to this model, when suggesting to add++code> flexWrap: wrap++/code> to fix the problem.

This description fits for words magnets on fridge, but...not for a React Native text.

offscreen9

 React Native Text is not made with Fridge Magnets!

Because if we re-read the React Native documentation:

The <Text> element is unique relative to layout: everything inside is no longer using the Flexbox layout but using text layout. This means that elements inside of a <Text> are no longer rectangles, but wrap when they see the end of the line.

A ++code>Text++/code> always wraps.  At least as soon as it is long enough. There is no need to add a ++code>flexWrap++/code> property: the StackOverflow answer is partially wrong for this. 

In our mental model, it means:

  • A ++code>Text++/code> has a content-driven width when it is shorter than the width of its parent.
offscreen10
  • Note: It's not easy to observe this in React Native: texts are almost always nested in a ++code>View++/code> were ++code>alignContent++/code> is set to ++code>strech++/code>, forcing the children to follow their parent width. On the screenshot above, I added ++code>alignSelf: flex-start++/code> on the ++code>Text++/code> element.
  • Then the ++code>Text++/code> turns to have a parent-driven width as soon as it reaches the end of the parent box: 
offscreen11
  • Note: height is always content-driven by default in a ++code>Text++/code> : fitting exactly to the words, without extra space below.
  • Even setting the ++code>numberOfLines ++/code>to 1 would not turn the text to one very long line: we would just had an ellipsis at the end of the text. The width of the Text is still parent-driven.
offscreen12
  •  The only way to achieve this in React Native is to set ++code>position: absolute ++/code>on the ++code>Text++/code> element on a flex-row container - quite the struggle, and definitely not the default...
offscreen13

 

So by default, a long text is never able to compute its width on its own.
Unless using absolute position, it's always given by its parent.

This point is important for what's coming next.

2.3 Auto-size of a child

Our title is not only a ++code>Text++/code>, but also a child in a ++code>View++/code> that uses the Flexbox layout. Given this, how is its size computed?

Once again, the parent vs content-driven model helped me to understand what was happening; I'll expose it to you here. I start with a classic ++code>View++/code> in column, because I find it easier to get:

offscreen14

Analyzing an Element in a column View

The blue element has a parent-driven width. It also has a content-driven height: fits to what is inside only.

  • The parent-driven width is due to the ++code>alignItems: stretch++/code> by default on the parent container; if we set it to ++code>flex-start ++/code>or ++code>center++/code>, the width of our element will be back to its own, driven by its content.
  • The content-driven height is a little more subtle in my sense; it's due to the ++code>flexGrow++/code> and ++code>flexShrink++/code> I already talked about, set to 0 by default. Our element does not adapt to its context; it does not look at its parent or siblings height: it looks only on itself and does what he "wants". If we set ++code>flex:1++/code>, element will start to "look around", "talk" with its siblings and parent, and grow and shrink accordingly.

If we transpose it exactly in a ++code>View++/code> with a row layout:

offscreen15

Analyzing an Element in a row View

  • The blue element has a fully content-driven width this time : it fits exactly what's inside him.
  • It has also a parent-driven height : it goes to the top and bottom.

So our title, as an element in a row, does not take its parent into account for its width. It follows only its content.

And...we arrive to a problem.

2.4 The Text-in-a-row paradox

If we resume what we learned above about our title:

  • Because it's a text, its width is always set by its parent.
  • But because it's an element in a row, its width is always set by its content.

   .....So....What now ?

https://thumbs.gfycat.com/BriskGentleAgama-small.gif

The "No it's you!!" Fight

2.5 The final REvelation

The React Native answer is pure and simple:

A ++code>Text++/code> always takes its parent's width

Even in a row container. Even if ++code>flexGrow++/code> is not set to 1, which is the official way to force an element to reach its parent size.

Note: To be precise, it's not exactly its parent's width: it's the size the ++code>Text++/code> would have had if it had been alone in its container. It means if there is a padding inside the parent, the ++code>Text++/code> width will be its parent width minus the padding. 

In our case, the ++code>title++/code> element always takes the ++code>titleContainer++/code> width, even with the icon before. It becomes obvious when we drastically reduce the ++code>titleContainer++/code> width: see what happens to the title!

offscreen16

The Text-In-A-Row follows its parent's Width!

But Text-In-A-Row certainly keeps some of the Element-In-A-Row properties : it still ignores completely its row siblings, and goes out of bounds regularly. It just takes its parent width, and then lives its life on its own...

...until someone tells him to stop with a good old ++code>flexShrink: 1++/code>.

https://media1.giphy.com/media/zCpYQh5YVhdI1rVYpE/giphy.gif

A ++code>flexShrink ++/code>ordering a Text to behave,
look at its Siblings and reduce its Size, finally!

And here we are, at the end of our journey; back to the StackOverflow pure answer - hopefully, with a better understanding of the elements we use.

 

Conclusion

Thank's for reading me so far! I hope you enjoyed following my deductions and discoveries in React Native layout. Now you also know the "Truth" about React Native Text...it always takes its parent's size, even when there is other siblings around! But if you had to remember one thing, it's the parent vs content-driven model: really, it's the best way to solve a ++code>flexbox ++/code>mystery. Stay tuned, because it may not be the last time you hear about it!

Développeur mobile ?

Rejoins nos équipes