Use a React component from Vox
Use a React component from Vox
Section titled “Use a React component from Vox”Vox compiles component declarations to TypeScript/React. You can import an
external React component or hook from any npm package or local .tsx file and
render it inside a Vox view: — it compiles to an ordinary ES import plus a JSX
tag, with no FFI or marshaling.
Import forms
Section titled “Import forms”// vox:skip — illustrative import surface (paths are project-specific)import react Button from "@acme/ui/Button" // defaultimport react { Dialog, DialogContent } from "@radix-ui/react-dialog" // namedimport react { Button as AcmeButton } from "@acme/ui" // aliasedimport react * as Dialog from "@radix-ui/react-dialog" // namespaceThe module specifier is emitted verbatim — Vox does no path rewriting; your
bundler’s package.json exports resolution does the work.
Rendering
Section titled “Rendering”Reference an imported component in a view: using call syntax. It lowers to a
JSX tag:
// vox:skip — placeholder package `@acme/ui`; validated copy lives at examples/golden/react_interop.voximport react { Button } from "@acme/ui"
component Page() { view: column() { Button() }}emits (web target):
// vox:skip — emitted output, not Vox sourceimport { Button } from "@acme/ui";
export function Page(): React.ReactElement { return ( <div> <Button /> </div> );}The compiler:
- emits the ES import once (grouped per module for named imports),
- renders
Button()→<Button/>(it is not mistaken for an HTML element and does not get a spurious sibling./Buttonimport), - passes props straight through — props are a plain JS object.
The same works on the React Native target: the import is emitted and the
component renders as a tag (the RN target maps Vox primitives like column/text
to View/Text, but external components pass through unchanged).
Required CSS and providers are handled for known libraries
Section titled “Required CSS and providers are handled for known libraries”Vox keeps a table of common component libraries
(external_libs.rs).
When you import one, the emitter:
- auto-injects required CSS files. Importing
@mantine/coreemitsimport "@mantine/core/styles.css";for you (Mantine ships real CSS, not runtime CSS-in-JS). - emits provider setup guidance for libraries that mandate a top-level
provider. Importing
@mantine/core,@chakra-ui/react,react-native-paper, ortamaguiemits a comment reminding you to mount<MantineProvider>/<ChakraProvider>/<PaperProvider>/<TamaguiProvider>at your app root.
Headless libraries (Radix, Headless UI, React Aria) need neither, and the emitter correctly adds nothing for them.
The scaffolded vite.config.ts also sets resolve.dedupe: ["react","react-dom"]
so the app and the imported library share a single React copy (duplicate copies
cause React’s “Invalid hook call” error).
Type checking
Section titled “Type checking”Vox treats an imported component as opaque: it does not type-check the props you
pass against the library’s .d.ts. That check is performed by the downstream
tsc against the genuine type definitions — exactly as a plain .js import
would behave. A misspelled prop surfaces when you build the emitted TypeScript.
Current limitations
Section titled “Current limitations”- Namespace member tags are not rendered as elements.
import react * as Dialog from "@radix-ui/react-dialog"emits the namespace import, but using a member as a tag (Dialog.Root()) lowers to a call expression, not<Dialog.Root/>. For Radix-style component sets, use the named form (import react { Dialog, DialogContent } from "@radix-ui/react-dialog"), which renders as tags. - Dependencies are not added to
package.jsonfor you. In app mode thepackage.jsonis yours to own; install the package and its peers (the provider-guidance comments name the relevant peers).