This came up in a conversation with a team recently, and it is a question I see surfacing again and again. They already had the hard part sorted: a headless CMS feeding an experience layer that handled delivery, personalisation, and A/B testing, with content nicely centralised.
What they were wrestling with sat at the front of the house, where they had a public website, a logged-in portal, and a native mobile app, each with its own demands. Their instinct, and it is a very natural one, was to build a single set of components and reuse them across all three. That instinct is worth resisting.
Why the reuse instinct is so strong
The logic feels airtight. You have put real effort into a design system, you have components that work, and rebuilding them per channel looks like waste, so the goal slowly becomes maximum reuse: one component, every surface. It is the same story that makes headless attractive, which is precisely why it is so easy to stretch it one step too far. Centralised content really does serve every channel well, but a centralised UI component does not, because the channels themselves are not the same thing.
Where it breaks
A web component carries all the assumptions of the web with it: click targets, hover states, viewport widths, the way a page scrolls. A native component lives by different rules: touch instead of click, gesture navigation, platform conventions the user already expects, and performance constraints a web component was never built for. Those are real differences, not cosmetic ones you can smooth over with a shared abstraction.
When you force one component to serve both, you usually end up with one of three results:
The web experience bends to fit constraints it does not actually have
The native experience inherits patterns that feel wrong on a phone
Both quietly degrade while the shared component fills up with conditionals trying to be
everything at once
The effort you spent chasing reuse comes straight back to you as friction, in every channel that component touches.
Reuse the layers that really are channel-agnostic
This isn't an argument against reuse. It's an argument for moving it to the layer where it actually holds. Underneath any channel there is a whole stack of logic that genuinely does not care what renders it.
- 1
The business layer: Your rules, your integration with the content layer, the work of turning raw content and services into something the application can use. None of that changes between web and native.
- 2
The view models: The shaped, ready-to-render data each screen needs. A single view model can feed a web component and a native component without either one needing to know the other exists.
Share those layers as widely as you can, and then let each channel have the components it deserves: web components tuned for web, native components tuned for the device. You reuse the thinking and you build the rendering per channel. It sounds like more work up front, and on day one it is, but it removes the compromise tax you would otherwise keep paying on every screen, in every channel, for as long as the product lives.
A simple test helps when you are unsure where something belongs. If a piece of code has to ask "which channel am I on?" to do its job, it is probably a component. If it never has to ask, it belongs down in the business or view model layer where every channel can lean on it.
When duplicating something is the smarter move
There is a quieter point underneath all of this, and it is the one I care about most. Centralising something only earns its keep while it serves every channel well, so the moment a shared piece stops giving a channel what that channel actually needs, duplicating it becomes the disciplined call rather than a lazy one. A team that duplicates a feature so the mobile app can do it properly has not thrown away the composable principle, they have applied it where it pays and stepped around it where it would cost more than it returns. Treating "reuse everywhere" as an absolute is how you end up with an architecture that serves the diagram instead of the user.
I tend to favour reusing what is already there and writing as little custom code as I sensibly can, so I do not say "go and duplicate this" lightly. The point is that the decision should follow the channel's needs, not a rule on a slide.
A cheaper way to find out
Sometimes you genuinely do not know yet whether a channel earns a full native build, and there is a pragmatic middle step worth keeping in your back pocket. You wrap the existing web experience in a native shell with a web view, ship it, and watch how people actually use it. You get a mobile presence quickly, with full reuse of what you already have, and you collect real usage data instead of guessing at it.
If the numbers justify a deeper native investment later, you build it with evidence behind the decision. If they do not, you have saved yourself the cost of a native build nobody needed, and either way the data makes the call instead of an assumption.
The thread that ties it together
What you are really after is a coherent experience on every channel, built on as much shared foundation as that goal allows, with reuse serving that aim rather than driving it. Get the layering right and most of your system ends up shared by default, with the channel-specific work concentrated exactly where the channels genuinely differ.
So if your team is mapping out a multi-channel build, the question I would put on the table is not how much you can reuse. It’s what is genuinely the same across your channels, and what only looks the same until you ship it. Answering that one honestly usually redraws the architecture in a healthier place.
We have this conversation with teams working through exactly this trade-off, and I always enjoy it. If it is live for you right now, I am happy to compare notes.
Book a call with us