An inbox and a conversation never appear together on a phone. Treating them as one component with a flag fakes navigation badly; giving each its own screen lets the platform do it properly.
The messaging tab in a marketplace app has two jobs. It shows the list of conversations, and it shows one conversation. On a wide screen those can sit side by side. On a phone they cannot, and the way that conflict is resolved decides whether the feature feels solid or quietly broken.
The tempting implementation is one screen with a mode. The inbox and the thread are both in the markup, a piece of state says which is active, and CSS hides the other. It is fewer files, it shares a header, and it demos well. I have built it that way, and I have since replaced it with two screens in every app where it existed. This piece is about why.
None of the failures is dramatic. Each is small, intermittent and hard to reproduce, which is exactly what makes the pattern expensive.
Each of these can be patched. The patches accumulate into a component whose real behaviour lives in a set of conditions nobody can hold in their head, and the next change breaks one of them.
The structure I use now treats the inbox and the thread as separate screens with separate addresses. The inbox is a list of conversation rows: an avatar, the other person's name, the last message, a timestamp and an unread marker. Tapping a row navigates to the thread, which is a different screen with its own layout: a header with a back control and the other person's name, a scrolling message area, and a message bar pinned to the bottom of that screen only.
The difference is not cosmetic. Because the thread is a navigation, it gets a history entry. The system back gesture returns to the inbox because there is an inbox behind it. The inbox restores its own scroll position because it is its own document state. The message bar cannot appear over the inbox, because the inbox does not contain one. There is nothing to hide, because there is only ever one thing on screen.
The class names stay the same across every app that has this feature, so they can be read side by side and a fix found in one can be carried to the others. That matters more than it sounds: the same two-screen structure, copied faithfully, means the same bugs are fixed once.
Once the thread is a screen, its layout can be designed for exactly one thing. The header sits at the top, the message bar at the bottom, and the message list takes the space between and is the only thing that scrolls. Getting that right depends on how the surrounding app scrolls, which is why the height of the thread is derived for each app rather than copied. An app whose document scrolls needs the thread to claim a fixed share of the viewport. An app whose shell already owns the scrolling needs the thread to fill its container, and copying the first app's value into the second creates a third scroller that pushes the tab bar off the screen.
On a phone, the keyboard is the other constraint. When it opens, the visible area shrinks, and a message bar fixed to the layout viewport can end up behind it. Keeping the bar inside the thread's own flex layout, rather than fixed to the window, lets it move with the space that is actually available. The last message should stay in view as the keyboard opens, which is a matter of scrolling the message area to the end when its size changes, not of guessing a keyboard height.
The thread's back control has an obvious destination, the inbox, so it is always present on the thread. The inbox, reached from the tab bar, has nothing behind it within the messaging feature, so it does not draw a back control at all. That follows a rule I apply everywhere: a control appears only while it leads somewhere. A back arrow on the inbox that sends the user to the home page, or does nothing, is a control that lies about what it does, and that is worse than no control.
Splitting the screens also makes the data access easier to reason about. The inbox reads one small, limited list of the signed-in user's own conversations, enough to draw the rows. The thread reads the messages of one conversation, again with a limit, and only when it is opened. Neither holds a live listener open in the background; a conversation is fetched when it is viewed and when a message is sent. With one screen holding both states, it is very easy to end up fetching the thread data for whichever conversation was last open while the user is looking at the inbox, and paying for reads that are never shown.
When a component has two states that never appear together on the target device, and each has its own layout, its own scroll and its own controls, those are two screens. Giving them separate addresses lets the platform do the work a mode flag was trying to fake: history, back, scroll restoration and focus all behave correctly because they are being used for what they were built to do. The code is slightly longer and the behaviour is much shorter to describe, which is the trade I would make every time.
Written by Liana Grigory, Entrepreneur and Software Engineer, from work on The Care Royal, Tegula Stone and Unified Savers. Everything above describes decisions actually made on those systems, including the ones that turned out to be wrong.