
1/9: Scaling components: how to build a true cross platform unified library between React, React Native and Email
A guide into a truly multi platform ui components architecture
Last Monday at 5:51 PM
Part 1 of a series. The rest: 2. The Brand That Finally Fit · 3. We Built the Fork, Proved It Worked, Then Watched It Fail in Production Anyway · 4. The Editor That Can't Come Home · 5. FormulaikMP: Why a Form Library Is Just a Resolver in a Trenchcoat · 6. Merging Two Auth Flows That Had Quietly Become Strangers · 7. MPNavigator: The Smallest Router That Could Possibly Work · 8. Injecting the Rest of the App In · 9. The Mirror-and-Shim Migration Pattern
We had a Button. Then we had three of them.
Not on purpose. web/main had a Button. app/main had a Button — same name, same rough intent, subtly different props, its own bugs, its own drift. templates, which renders transactional email out of React, had a Button too, because obviously <button onClick> doesn't survive contact with an email client. Three components, three teams of one maintaining them by accident, three places a design change had to land separately and usually didn't. Multiply that by every field type in a signup form, every stage of an account flow, and you get the actual motivation for @peakub/components: not "wouldn't it be nice to share code," but "we are already paying the cost of not sharing it, continuously, and it's invisible until someone asks why the mobile signup button looks like it's from 2019."
This is the story of building that library — the bet we made early, the parts that worked immediately, the one that didn't, and why the thing that replaced it is better than what we would have designed on paper.
The bet: primitives, not forks
The obvious way to share components across web, native, and email is file-extension polymorphism — button.web.js, button.native.js, button.email.js, resolved automatically by each platform's bundler. It's a real, well-worn pattern. It's also a trap if you reach for it by default: every component that could be shared but has even one platform-specific line ends up with three files, three implementations to keep behaviorally in sync, and the "shared library" quietly becomes three libraries wearing a trenchcoat.
We used the fork mechanism for exactly one thing: genuine, irreducible divergence. button.email.js is a real fork — email clients don't run JavaScript, so there's no onPress, no disabled state, no icons that survive Outlook's rendering engine. It's built on @react-email/components, not on anything web or native share. That's what a fork is for.
For everything else, we made a different bet: a small set of global primitives, defined once, that every component is written against instead of a real DOM element or a real React Native component.
// bootstrap/webGlobals.js
globalThis.MPView = forwardRef((props, ref) => {
if (props.isSquircle && Squircle) {
return <Squircle ref={ref} {...props}>{props.children}</Squircle>;
}
return <div ref={ref} {...props}>{props.children}</div>;
});// bootstrap/nativeGlobals.js
globalThis.MPView = forwardRef(({ children, isSquircle, cornerRadius, ...rest }, ref) => (
<View ref={ref} {...rest}>{children}</View>
));A component written against MPView never knows or cares whether it's rendering a <div> or a React Native View. It's one file. It ships to both platforms. The platform difference lives in exactly one place — the bootstrap — instead of being smeared across every component that happens to need a box on the screen.
The primitive list stayed deliberately small: MPView, MPText, MPSpan, MPH1–MPH5, MPPressable, MPSpinner, MPTextInput, MPCheckbox, MPLink, MPFormulaik, and — later — MPNavigator. Everything in the library is built out of those, plus Tailwind class strings (which are just text; each platform's own styling pipeline, real Tailwind on web, NativeWind on native, decides what they mean).
Proving it: a Button that isn't lying about being shared
The easy failure mode for a component library is writing components that compile on every platform but that nobody actually uses — smoke tests standing in for real integration. So the rule we held ourselves to was: every primitive, every shared component, gets wired into a real call site in each consumer before we call it done. Not a Storybook story. A production screen.
That surfaced real work immediately. MPPressable — the primitive behind every clickable thing — had to support both onPress (the React Native convention) and onClick (the web one), because forcing one platform's naming onto the other's natural callers is exactly the kind of paper cut that makes a "shared" library feel foreign on one of its two homes:
globalThis.MPPressable = forwardRef(
({ onPress, onClick, disabled, isSquircle, cornerRadius = 18, testId, ...props }, ref) => {
const handleClick = (e) => {
e?.preventDefault?.();
e?.stopPropagation?.();
if (disabled) return;
onPress?.(e);
onClick?.(e);
};
// ...
},
);Both fire if both happen to be passed. Nobody has to remember which platform they're integrating into just to attach a click handler.
Wiring it for real also caught a bug that a smoke test never would have: a web/widgets card component whose click handlers had gone dead — MPPressable's squircle branch wasn't forwarding onClick at all. It had presumably been broken since whoever wrote it last touched it, silently, because nothing had ever actually clicked the thing in anger.
Forms without forking
Forms are where "just use primitives" gets tested hardest, because a form library isn't one component, it's a resolver: given a field's declared type, produce the component that renders it. We built MPFormulaik as a global resolving to @formulaik/react (web) or @formulaik/react-native (native) — same API, different engine — and a shared FormulaikMP component library sitting on top, one file per field type, each one just another consumer of MPTextInput/MPCheckbox/MPPressable.
The public documentation for the underlying form library describes its components prop as accepting a map — { input, submit, checkbox }. We tried that. Every field rendered nothing. No error, no warning, just an empty form.
Subscribe to continue reading
Peakub is a home for creators, publishers, and teams to publish, grow, and monetize their work — all in one place. We're building a fast, privacy‑respecting platform that helps you focus on your craft while Peakub handles the plumbing — from content tooling to delivery and analytics.


