@firsthandjs/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Firsthand contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # @firsthandjs/react
2
+
3
+ React components inside Firsthand — for MUI, and for every other library that
4
+ exists only as React components.
5
+
6
+ **Documentation:** [guide](https://github.com/firsthandjs/firsthand/blob/main/docs/guide/11-react-interop.md) · [API reference](https://github.com/firsthandjs/firsthand/blob/main/docs/reference/react.md) · [all docs](https://github.com/firsthandjs/firsthand/blob/main/docs/README.md)
7
+
8
+ ```
9
+ npm install @firsthandjs/react react react-dom
10
+ ```
11
+
12
+ 0.66 kB gzip. React and react-dom are peer dependencies: an application that
13
+ never imports this package does not install them.
14
+
15
+ ```tsx
16
+ import Button from '@mui/material/Button';
17
+ import { fromReact } from '@firsthandjs/react';
18
+
19
+ const MuiButton = fromReact(Button);
20
+
21
+ <MuiButton variant="contained" onClick={save}>
22
+ Save
23
+ </MuiButton>;
24
+ ```
25
+
26
+ That works. [`integrations/interop`](../../integrations/interop) builds it and
27
+ drives it in Chromium: MUI's `Button`, `Chip` and `Slider`, a React event
28
+ calling back into a Firsthand signal, and a Firsthand signal updating a React prop.
29
+
30
+ ## React components as elements
31
+
32
+ ```tsx
33
+ import '@firsthandjs/react/auto'; // once, at startup
34
+ import Button from '@mui/material/Button';
35
+
36
+ <Button variant="contained" onClick={save}>
37
+ Save
38
+ </Button>;
39
+ ```
40
+
41
+ Same bridge, same cost, one import instead of one wrapper per component. Use
42
+ `fromReact` where you need Firsthand children — a direct element types `children`
43
+ as React's `ReactNode` — or the bridge's `host` and `class` props.
44
+
45
+ ## One provider for every root
46
+
47
+ Each bridge mounts its own React root, so React context does not flow between
48
+ them: a `ThemeProvider` rendered through one bridge cannot reach a button
49
+ rendered through another.
50
+
51
+ ```tsx
52
+ import { createElement } from 'react';
53
+ import { ThemeProvider } from '@mui/material/styles';
54
+ import { setReactWrapper } from '@firsthandjs/react';
55
+
56
+ setReactWrapper((node) => createElement(ThemeProvider, { theme: muiTheme(theme.value) }, node));
57
+ ```
58
+
59
+ Reading a signal in the wrapper makes one theme switch re-render every bridged
60
+ component, and nothing else.
61
+
62
+ ## What it costs
63
+
64
+ React and react-dom are **about 45 kB gzip**, eight times this framework's
65
+ entire runtime. Everything below a bridge is React's: its reconciler, its
66
+ re-renders, its synthetic events. The bridge is fine-grained on the Firsthand side
67
+ only — one effect re-renders the React root when a prop it reads changes, and
68
+ the subtree below then behaves exactly as it does in a React application.
69
+
70
+ So this is an escape hatch, and the size of the thing you are escaping to
71
+ should decide whether to use it:
72
+
73
+ | You want | Use |
74
+ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | A button, a card, a layout | Plain elements, or [`@firsthandjs/styled`](../styled) |
76
+ | A design system | A **web-component** one: Shoelace, Material Web, Fluent, Vaadin, Carbon. They are custom elements, so Firsthand uses them with nothing in between |
77
+ | A date picker, a data grid, a rich text editor that only exists for React | This package |
78
+
79
+ The middle row is the one people miss. A web-component library needs no bridge,
80
+ no adapter and no React — see the interop integration, which uses Shoelace with
81
+ nothing but an import.
82
+
83
+ ## How it behaves
84
+
85
+ **Call `fromReact` once, at module level.** Each _instance_ mounts its own
86
+ React root, so creating the bridge inside a component would create a root per
87
+ render of the surrounding page.
88
+
89
+ **Props are forwarded, except two.** `class` goes on the host element, `host`
90
+ chooses the host tag (a `span` by default, so inline layout survives).
91
+ Everything else reaches the React component untouched — including functions, so
92
+ callbacks work in both directions.
93
+
94
+ **Children stay Firsthand's.** React cannot render DOM nodes, and a Firsthand dynamic
95
+ child is a deferred part rather than a node, so the bridge gives Firsthand its own
96
+ element inside the React tree and fills it through the ordinary `insert`.
97
+ Reactive children update without React hearing about it:
98
+
99
+ ```tsx
100
+ <MuiCard>
101
+ <p>{count.value}</p> {/* updates as a text write, not a React render */}
102
+ </MuiCard>
103
+ ```
104
+
105
+ **Disposal unmounts the root**, in a microtask — React refuses to unmount a
106
+ root while it is rendering, and a Firsthand disposal can happen inside an effect a
107
+ React event started.
108
+
109
+ ## `ReactHost`
110
+
111
+ For a component chosen at runtime:
112
+
113
+ ```tsx
114
+ <ReactHost component={whichever} props={{ variant: 'contained' }} host="div" />
115
+ ```
116
+
117
+ It mounts a root per instance either way, so prefer `fromReact` where the
118
+ component is known.
119
+
120
+ ## Two copies of React
121
+
122
+ If you link this package from a monorepo rather than installing it, your
123
+ bundler may resolve its `react` import and your application's to two different
124
+ copies, and React's hooks dispatcher will be `null`:
125
+
126
+ ```
127
+ TypeError: Cannot read properties of null (reading 'useContext')
128
+ ```
129
+
130
+ ```ts
131
+ // vite.config.ts
132
+ resolve: {
133
+ dedupe: ['react', 'react-dom'];
134
+ }
135
+ ```
136
+
137
+ An installed package has one copy and needs none of this.
package/dist/auto.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { ComponentType } from 'react';
2
+ declare global {
3
+ namespace JSX {
4
+ interface ForeignElementTypes {
5
+ react: ComponentType<any>;
6
+ }
7
+ }
8
+ }
9
+ //# sourceMappingURL=auto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto.d.ts","sourceRoot":"","sources":["../src/auto.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAO3C,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,GAAG,CAAC;QACZ,UAAU,mBAAmB;YAE3B,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC;SAC3B;KACF;CACF"}
package/dist/auto.js ADDED
@@ -0,0 +1 @@
1
+ import{b as e}from"./chunk-OSGUVKQH.js";import{setComponentAdapter as n}from"@firsthandjs/dom";n(o=>e(o));
@@ -0,0 +1 @@
1
+ import{bind as R,onCleanup as g,signal as h}from"@firsthandjs/core";import{component as m}from"@firsthandjs/dom";import{insert as T}from"@firsthandjs/dom/internal";import{createElement as s}from"react";import{createRoot as w}from"react-dom/client";var u=h(null);function x(e){u.value=e}function M({slot:e}){return s("span",{ref:t=>{t!==null&&e.parentNode!==t&&t.appendChild(e)}})}function N(e,t={}){let f=n=>{let y=n.host??t.host??"span",l=document.createElement(y),c=w(l),a="children"in n?document.createElement("span"):null;return a!==null&&T(a,()=>n.children),R(()=>{let o={};for(let r of Object.keys(n))r==="class"||r==="host"||r==="children"||(o[r]=n[r]);typeof n.class=="string"&&(l.className=n.class),a!==null&&(o.children=s(M,{slot:a}));let d=s(e,o),p=u.value;c?.render(p===null?d:p(d))}),g(()=>{let o=c;c=null,queueMicrotask(()=>{o?.unmount()})}),l},i=e.displayName??(e.name===""?"Anonymous":e.name);return m(f,void 0,`firsthand/react:${i}`,`React(${i})`)}var C=m(e=>N(e.component,{...e.host===void 0?{}:{host:e.host}})({...e.props}),void 0,"firsthand/react:ReactHost","ReactHost");export{x as a,N as b,C as c};
@@ -0,0 +1,64 @@
1
+ import { type Component, type View } from '@firsthandjs/dom';
2
+ import { type ComponentType, type ReactNode } from 'react';
3
+ /** Wraps what every bridged root renders — a provider tower, usually. */
4
+ export type ReactWrapper = (node: ReactNode) => ReactNode;
5
+ /**
6
+ * Wraps every bridged React root in the same React elements.
7
+ *
8
+ * Each bridge mounts its **own** React root, so React context does not flow
9
+ * from one to another: a `<ThemeProvider>` written as one bridged component
10
+ * cannot reach a `<Button>` written as another, because they are different
11
+ * trees. Providers therefore have to be per root, which is what this does.
12
+ *
13
+ * ```tsx
14
+ * import { createElement } from 'react';
15
+ * import { ThemeProvider } from '@mui/material/styles';
16
+ *
17
+ * setReactWrapper((node) => createElement(ThemeProvider, { theme: muiTheme(theme.value) }, node));
18
+ * ```
19
+ *
20
+ * Reading a signal inside the wrapper — `theme.value` above — is an ordinary
21
+ * reactive read, so swapping the theme re-renders every bridged component and
22
+ * nothing else. `null` removes it again.
23
+ */
24
+ export declare function setReactWrapper(next: ReactWrapper | null): void;
25
+ /**
26
+ * Props a bridged component takes.
27
+ *
28
+ * The React component's own, except `children`: what you write inside a
29
+ * bridged component in TSX is Firsthand's, not React's — DOM that Firsthand keeps
30
+ * owning inside the React tree — so the type says `View` rather than
31
+ * `ReactNode`.
32
+ */
33
+ export type BridgeProps<P> = Omit<P, 'children'> & {
34
+ readonly children?: View;
35
+ /** Put on the element React renders into, not passed to the component. */
36
+ readonly class?: string;
37
+ /** The element to mount into. `span` by default, so inline layout survives. */
38
+ readonly host?: keyof HTMLElementTagNameMap;
39
+ };
40
+ /**
41
+ * Turns a React component into a Firsthand one.
42
+ *
43
+ * Call it once, at module level, next to the import — a bridge created inside
44
+ * a component would mount a new React root per instance.
45
+ */
46
+ export declare function fromReact<P extends object>(Component_: ComponentType<P>, options?: {
47
+ host?: keyof HTMLElementTagNameMap;
48
+ }): Component<BridgeProps<P>>;
49
+ /**
50
+ * The same thing without declaring a component first.
51
+ *
52
+ * ```tsx
53
+ * <ReactHost component={Button} props={{ variant: 'contained' }} />
54
+ * ```
55
+ *
56
+ * Useful when the component is chosen at runtime. It mounts a root per
57
+ * instance either way, so prefer `fromReact` where you can.
58
+ */
59
+ export declare const ReactHost: Component<{
60
+ readonly component: ComponentType<never>;
61
+ readonly props?: Record<string, unknown>;
62
+ readonly host?: keyof HTMLElementTagNameMap;
63
+ }>;
64
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAa,KAAK,SAAS,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAExE,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG1E,yEAAyE;AACzE,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,SAAS,CAAC;AAQ1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAE/D;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,qBAAqB,CAAC;CAC7C,CAAC;AAqBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EACxC,UAAU,EAAE,aAAa,CAAC,CAAC,CAAC,EAC5B,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,qBAAqB,CAAA;CAAO,GACnD,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CA2D3B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS;wBACA,aAAa,CAAC,KAAK,CAAC;qBACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;oBACxB,MAAM,qBAAqB;EAW5C,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{a,b,c}from"./chunk-OSGUVKQH.js";export{c as ReactHost,b as fromReact,a as setReactWrapper};
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@firsthandjs/react",
3
+ "version": "0.1.0",
4
+ "description": "Render React components inside Firsthand, for component libraries like MUI.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "./dist/auto.js"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./auto": {
16
+ "types": "./dist/auto.d.ts",
17
+ "default": "./dist/auto.js"
18
+ }
19
+ },
20
+ "types": "./dist/index.d.ts",
21
+ "main": "./dist/index.js",
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "dependencies": {
28
+ "@firsthandjs/core": "0.1.0",
29
+ "@firsthandjs/dom": "0.1.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=20.11.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "provenance": true
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/firsthandjs/firsthand.git",
41
+ "directory": "packages/react"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/firsthandjs/firsthand/issues"
45
+ },
46
+ "homepage": "https://github.com/firsthandjs/firsthand#readme",
47
+ "keywords": [
48
+ "firsthand",
49
+ "react",
50
+ "interop",
51
+ "mui",
52
+ "adapter"
53
+ ],
54
+ "peerDependencies": {
55
+ "react": ">=18",
56
+ "react-dom": ">=18"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "react": {
60
+ "optional": false
61
+ },
62
+ "react-dom": {
63
+ "optional": false
64
+ }
65
+ }
66
+ }