@wingleeio/mugen-native
v0.10.1
Published
React Native renderer for mugen — virtualized lists with analytic row heights, measured with pretext-native, never the host layout.
Maintainers
Readme
mugen-native
mugen for React Native: virtualized lists whose row heights are
computed from font tables — via
@wingleeio/pretext-native — never measured from the host.
Exact heights for never-mounted rows, zero measure-on-mount shift, pixel-exact
deep links, O(log n) offsets: the web guarantees, on Hermes.
It is a renderer, not a fork: the engine (walker, Fenwick offset index, row
slots, stick-to-bottom spring, animation clock) is imported from
@wingleeio/mugen/native-core, and every primitive's measure half is the web
implementation — heights cannot drift between platforms. What's native:
MugenVListdrives aScrollViewthrough a scroll adapter (onLayoutinstead of ResizeObserver, drag gestures instead of wheel events).Textpaints pretext's materialized lines — each measured line is its own single-line<Text>ati × lineHeight. RN's line breaker (CoreText/Minikin) never gets a vote, so paint can't disagree with the measured height.VStack/HStack/Escape/Collapserender asViews; anHStacksplits width with the samedistributemath the measure ran.
Install
npm i @wingleeio/mugen-nativeRequires React 18.2/19 and React Native ≥ 0.72.
Quick start
import {
configureMugenNative,
MugenVList,
Text,
VStack,
useMugenVirtualizer,
} from '@wingleeio/mugen-native';
// Once at startup: the same TTFs the app paints with feed the measurement.
configureMugenNative({ fonts: [{ family: 'Inter', weight: 400, data: interTtfBytes }] });
function Inbox({ messages }: { messages: Message[] }) {
const list = useMugenVirtualizer({ items: messages });
return (
<MugenVList
instance={list}
getKey={(m) => m.id}
font="16px Inter"
lineHeight={22}
stickToBottom
render={(m) => (
<VStack gap={4} padding={12}>
<Text font="600 15px Inter">{m.author}</Text>
<Text>{m.body}</Text>
</VStack>
)}
/>
);
}On Android (and for multi-weight fonts generally), map faces explicitly:
configureMugenNative({
fonts,
fontFaceResolver: ({ family, weight }) =>
family === 'Inter' ? { fontFamily: `Inter_${weight}` } : { fontFamily: family },
});See apps/native-example for a complete Expo app
(streaming markdown chat, stick-to-bottom, collapse).
React Compiler
mugen's walker calls your row components during the measure pass, outside
React's render — there is no dispatcher there. The React Compiler's injected
memo cache (useMemoCache) needs one, so compiled row components crash with
dispatcher.useMemoCache is not a function. Opt row-authoring modules out
(Expo SDK 54+ enables the compiler by default):
// components/rows.tsx — everything rendered inside MugenVList's `render`
'use no memo';Inside rows, use the row-scoped hooks (useMugenRow) instead of React state —
they live in the list instance and survive virtualization.
Develop
pnpm --filter @wingleeio/mugen-native test # node tests — react-native stubbed, fonts hermetic
pnpm --filter @wingleeio/mugen-native check-types
pnpm --filter @wingleeio/mugen-native build