@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 +21 -0
- package/README.md +137 -0
- package/dist/auto.d.ts +9 -0
- package/dist/auto.d.ts.map +1 -0
- package/dist/auto.js +1 -0
- package/dist/chunk-OSGUVKQH.js +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +66 -0
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 @@
|
|
|
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};
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|