Skip to content

Commit d8649ec

Browse files
committed
feat: Svelte Context between reactified Svelte components
1 parent b3aa424 commit d8649ec

16 files changed

+140
-45
lines changed

docs/caveats.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,15 @@ React vDOM output:
6868

6969
```jsx
7070
<Bridge>
71-
<SvelteToReactContext.Provider>
71+
<SvelteFirstContext.Provider>
7272
<p>
7373
<Bridge>
74-
<SvelteToReactContext.Provider>
74+
<SvelteFirstContext.Provider>
7575
<Child /> <- When merging the render trees the <h1>Content</h1> is injected here
76-
</SvelteToReactContext.Provider>
76+
</SvelteFirstContext.Provider>
7777
</Bridge>
7878
</p>
79-
</SvelteToReactContext.Provider>
79+
</SvelteFirstContext.Provider>
8080
</Bridge>
8181
```
8282

docs/reactify.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ type Props = {
1616
const Dialog: React.FC<Props> = ({ onClose }) => (
1717
<div className="dialog">
1818
<h2>Thanks for subscribing!</h2>
19-
<svelte.Button type="primary" onClick={() => onClose()}>
19+
<svelte.Button type="primary" onclick={() => onClose()}>
2020
Close
2121
</svelte.Button>
2222
</div>
2323
);
2424
```
2525

26-
React only has props, we we assume that the props starting with "on" followed by a capital letter are event handlers.
26+
## When React starts the rendering, rendering the children is delayed
27+
28+
This is because we want to extract the context from the Svelte component and provide that to the Svelte child components
2729

2830
## Svelte components missing CSS?
2931

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"type": "git",
1111
"url": "https://github.com/bfanger/svelte-preprocess-react.git"
1212
},
13-
"version": "2.0.6",
13+
"version": "2.1.0",
1414
"license": "MIT",
1515
"type": "module",
1616
"scripts": {

playwright/tests/react-first.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ test.describe("react-first", () => {
99
await page.goto("/react-spa.html");
1010
await testDog(page);
1111
await testChildren(page);
12+
await testContext(page);
1213
});
1314

1415
test("Hydrate reactified Svelte component inside React server rendered page", async ({
@@ -17,6 +18,7 @@ test.describe("react-first", () => {
1718
await page.goto("/react-ssr?hydrate=1");
1819
await testDog(page);
1920
await testChildren(page);
21+
await testContext(page);
2022
});
2123
});
2224

@@ -46,3 +48,8 @@ async function testChildren(page: Page) {
4648
}),
4749
).toHaveScreenshot();
4850
}
51+
async function testContext(page: Page) {
52+
await expect(
53+
page.locator("pre", { hasText: '"Svelte context value"' }),
54+
).toHaveScreenshot();
55+
}

src/demo/components/DebugContext.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
<script lang="ts">
22
import { getContext } from "svelte";
33
4-
export let id: any;
4+
type Props = {
5+
id: any;
6+
};
7+
let { id }: Props = $props();
58
69
const context = getContext(id);
710
const text = JSON.stringify(context);

src/lib/hooks.svelte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default function hooks<T>(
3535
});
3636

3737
onDestroy(() => {
38-
const index = parent.hooks.findIndex((h) => h === hook);
38+
const index = parent.hooks.findIndex((h) => h.key === hook.key);
3939
if (index !== -1) {
4040
parent.hooks.splice(index, 1);
4141
}

src/lib/internal/Bridge.svelte.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useRef, useState, useEffect, createElement } from "react";
22
import type { ReactDependencies, TreeNode } from "./types.js";
33
import Child from "./Child.js";
4-
import SvelteToReactContext from "./SvelteToReactContext.js";
4+
import SvelteFirstContext from "./SvelteFirstContext.js";
55
import portalTag from "svelte-preprocess-react/internal/portalTag.js";
66

77
type BridgeProps = {
@@ -71,7 +71,7 @@ function renderBridge(
7171
}
7272

7373
const vdom = createElement(
74-
SvelteToReactContext.Provider,
74+
SvelteFirstContext.Provider,
7575
{ value: node },
7676
children === undefined
7777
? createElement(node.reactComponent, props)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="ts">
2+
import { getAllContexts } from "svelte";
3+
4+
type Props = {
5+
setContexts: (value: Map<any, any>) => void;
6+
};
7+
const { setContexts }: Props = $props();
8+
9+
setContexts(getAllContexts());
10+
</script>

src/lib/internal/ReactFirstContext.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as React from "react";
2+
3+
/**
4+
* Context from a reactified parent component.
5+
*/
6+
const ReactFirstContext = React.createContext<
7+
| {
8+
/** Resolves, when the parent svelte component was mounted & contexts are extracted */
9+
promise: Promise<void>;
10+
contexts?: Map<any, any>;
11+
}
12+
| undefined
13+
>(undefined);
14+
ReactFirstContext.displayName = "ReactToReactContext";
15+
export default ReactFirstContext;

src/lib/internal/Slot.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as React from "react";
22
import portalTag from "./portalTag";
3-
import SvelteToReactContext from "./SvelteToReactContext";
3+
import SvelteFirstContext from "./SvelteFirstContext";
44

55
type Props = {
66
slot: number;
77
};
88
const Slot: React.FC<Props> = ({ slot }) => {
9-
const node = React.useContext(SvelteToReactContext);
9+
const node = React.useContext(SvelteFirstContext);
1010
const ref = React.useRef<HTMLElement>(undefined);
1111
const el = node?.slotSources[slot];
1212
React.useEffect(() => {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as React from "react";
2+
import type { TreeNode } from "./types";
3+
4+
/**
5+
* Context from a sveltified parent component.
6+
*/
7+
const SvelteFirstContext = React.createContext(
8+
undefined as TreeNode | undefined,
9+
);
10+
SvelteFirstContext.displayName = "SvelteFirstContext";
11+
export default SvelteFirstContext;

src/lib/internal/SvelteToReactContext.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/lib/internal/SvelteWrapper.svelte

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
import type React from "react";
66
import type { Component } from "svelte";
7+
import ExtractContexts from "svelte-preprocess-react/internal/ExtractContexts.svelte";
78
import portalTag from "svelte-preprocess-react/internal/portalTag";
89
910
type Props = {
@@ -12,9 +13,16 @@
1213
props: Record<string, any>;
1314
react$children?: React.ReactNode;
1415
setSlot?: (slot: HTMLElement | undefined) => void;
16+
setContexts?: (value: Map<any, any>) => void;
1517
};
16-
let { SvelteComponent, nodeKey, props, react$children, setSlot }: Props =
17-
$props();
18+
let {
19+
SvelteComponent,
20+
nodeKey,
21+
props,
22+
react$children,
23+
setSlot,
24+
setContexts,
25+
}: Props = $props();
1826
1927
(globalThis as any).$$reactifySetProps = (update: Record<string, any>) => {
2028
props = update;
@@ -41,7 +49,9 @@
4149
node={nodeKey}
4250
style="display:contents"
4351
use:slot
44-
></svelte-children>{/if}</SvelteComponent
52+
></svelte-children>{/if}{#if setContexts}<ExtractContexts
53+
{setContexts}
54+
/>{/if}</SvelteComponent
4555
>
4656
{/if}</svelte:element
4757
>

src/lib/reactify.ts

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as React from "react";
22
import { createRawSnippet, mount, unmount, type Component } from "svelte";
33
import { render } from "svelte/server";
4-
import SvelteToReactContext from "./internal/SvelteToReactContext.js";
4+
import SvelteFirstContext from "./internal/SvelteFirstContext.js";
5+
import ReactFirstContext from "./internal/ReactFirstContext.js";
56
import SvelteWrapper from "./internal/SvelteWrapper.svelte";
67
import type { ChildrenPropsAsReactNode } from "svelte-preprocess-react/internal/types.js";
78

@@ -52,21 +53,35 @@ function single<P extends Record<string, any>>(
5253
const sveltePropsRef = React.useRef<(props: P) => void>(undefined);
5354
const svelteChildrenRef = React.useRef<HTMLElement>(undefined);
5455
const reactChildrenRef = React.useRef<HTMLElement>(undefined);
55-
const node = React.useContext(SvelteToReactContext);
56-
const { key, context } = node ?? { key: "/island/" };
56+
const node = React.useContext(SvelteFirstContext);
5757

58-
// Mount the Svelte component
59-
React.useEffect(() => {
60-
const target = wrapperRef.current;
61-
if (!target) {
62-
return undefined;
63-
}
64-
const component = mount(SvelteWrapper, {
58+
const nestedContext = React.useContext(ReactFirstContext);
59+
const nestedRef = React.useRef(
60+
{} as {
61+
promise: Promise<void>;
62+
contexts?: Map<any, any>;
63+
resolve: () => void;
64+
},
65+
);
66+
if (typeof nestedRef.current.promise === "undefined") {
67+
nestedRef.current.promise = new Promise((resolve) => {
68+
nestedRef.current.resolve = resolve;
69+
});
70+
}
71+
const nodeKey = node?.key ?? "/island/";
72+
73+
const mountComponent = React.useCallback((target: HTMLElement) => {
74+
return mount(SvelteWrapper, {
6575
target,
6676
props: {
6777
SvelteComponent: SvelteComponent as any,
68-
nodeKey: key,
69-
react$children: children,
78+
nodeKey: node?.key ?? "/island/",
79+
react$children: node ? children : children ? [] : undefined,
80+
setContexts: node
81+
? undefined
82+
: (value: Map<any, any>) => {
83+
nestedRef.current.contexts = value;
84+
},
7085
props,
7186
setSlot: (el: HTMLElement | undefined) => {
7287
if (el && reactChildrenRef.current) {
@@ -75,12 +90,33 @@ function single<P extends Record<string, any>>(
7590
svelteChildrenRef.current = el;
7691
},
7792
},
78-
context,
93+
context: node?.context ?? nestedContext?.contexts,
7994
});
80-
sveltePropsRef.current = (globalThis as any).$$reactifySetProps;
95+
}, []);
8196

97+
// Mount the Svelte component
98+
React.useEffect(() => {
99+
const target = wrapperRef.current;
100+
if (!target) {
101+
return undefined;
102+
}
103+
let component: ReturnType<typeof mount>;
104+
if (nestedContext) {
105+
void nestedContext.promise.then(() => {
106+
component = mountComponent(target);
107+
nestedRef.current.resolve();
108+
});
109+
} else {
110+
component = mountComponent(target);
111+
nestedRef.current.resolve();
112+
}
113+
114+
sveltePropsRef.current = (globalThis as any).$$reactifySetProps;
82115
return () => {
83-
void unmount(component);
116+
void (async () => {
117+
await nestedContext?.promise;
118+
await unmount(component);
119+
})();
84120
};
85121
}, [wrapperRef]);
86122

@@ -113,7 +149,7 @@ function single<P extends Record<string, any>>(
113149
const len = $$payload.out.length;
114150
(SvelteWrapper as any)($$payload, {
115151
SvelteComponent,
116-
nodeKey: key,
152+
nodeKey,
117153
props,
118154
react$children: children,
119155
});
@@ -129,7 +165,7 @@ function single<P extends Record<string, any>>(
129165
}
130166
const result = render(SvelteComponent as any, {
131167
props,
132-
context,
168+
context: node?.context,
133169
});
134170
if (typeof result.head === "string") {
135171
html += result.head;
@@ -152,7 +188,7 @@ function single<P extends Record<string, any>>(
152188
React.createElement("reactify-svelte", {
153189
key: "reactify",
154190
ref: wrapperRef,
155-
node: key,
191+
node: nodeKey,
156192
style: { display: "contents" },
157193
suppressHydrationWarning: true,
158194
dangerouslySetInnerHTML: { __html: html },
@@ -163,7 +199,7 @@ function single<P extends Record<string, any>>(
163199
"react-children",
164200
{
165201
key: "react-children",
166-
node: key,
202+
node: nodeKey,
167203
style: { display: "none" },
168204
},
169205
children,
@@ -188,15 +224,24 @@ function single<P extends Record<string, any>>(
188224
{
189225
key: "react-children",
190226
ref: reactChildrenRef,
191-
node: key,
227+
node: node?.key,
192228
style: { display: "none" },
193229
},
194-
children,
230+
node
231+
? children
232+
: React.createElement(
233+
ReactFirstContext.Provider,
234+
{ value: nestedRef.current },
235+
children,
236+
),
195237
)
196238
: undefined,
197239
);
198240
},
199241
};
242+
if (name) {
243+
(named[name] as React.FC).displayName = name;
244+
}
200245
cache.set(SvelteComponent, named[name]);
201246
return named[name];
202247
}

0 commit comments

Comments
 (0)