Skip to content

Commit

Permalink
feat: Signal based reactivity
Browse files Browse the repository at this point in the history
  • Loading branch information
bfanger committed May 30, 2024
1 parent 3a6c3c6 commit e63fa2e
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 76 deletions.
6 changes: 6 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,11 @@
"prefer-const": "off",
},
},
{
"files": ["*.svelte.ts"],
"rules": {
"filenames/match-exported": ["warn", null, "\\.svelte$"],
},
},
],
}
7 changes: 5 additions & 2 deletions src/lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ export default function hooks<T>(

if (parent) {
const hook = { Hook, key: autoKey(parent) };
parent.hooks.update(($hooks) => [...$hooks, hook]);
parent.hooks.push(hook);
onDestroy(() => {
parent.hooks.update(($hooks) => $hooks.filter((entry) => entry !== hook));
const index = parent.hooks.findIndex((h) => h === hook);
if (index !== -1) {
parent.hooks.splice(index, 1);
}
});
} else if (ReactDOMClient) {
onDestroy(standalone(Hook, ReactDOMClient, renderToString));
Expand Down
2 changes: 1 addition & 1 deletion src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { default as useStore } from "./useStore.js";
export { default as reactify } from "./reactify.js";
export { default as sveltify } from "./sveltify.js";
export { default as sveltify } from "./sveltify.svelte.js";
export { default as used } from "./used.js";
export { default as hooks } from "./hooks.js";
45 changes: 32 additions & 13 deletions src/lib/internal/Bridge.ts → src/lib/internal/Bridge.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as React from "react";
import useStore from "../useStore.js";
import SvelteToReactContext from "./SvelteToReactContext.js";
import Child from "./Child.js";
import type { TreeNode } from "./types";
import type { TreeNode } from "./types.js";

export type BridgeProps = {
node: TreeNode;
Expand All @@ -13,15 +12,36 @@ export type BridgeProps = {
) => React.ReactPortal;
};
const Bridge: React.FC<BridgeProps> = ({ node, createPortal }) => {
const props = { ...useStore(node.props) };
props.key = "component";
let { children } = props;
const fresh = React.useRef(false);
const [result, setResult] = React.useState<React.ReactNode>(() =>
renderBridge(node, createPortal, true),
);
React.useEffect(
() =>
$effect.root(() => {
$effect(() => {
fresh.current = true;
setResult(renderBridge(node, createPortal, false));
});
}),
[],
);
if (fresh.current) {
fresh.current = false;
return result;
}
return renderBridge(node, createPortal, false);
};

function renderBridge(
node: TreeNode,
createPortal: BridgeProps["createPortal"],
initialRender: boolean,
) {
let { children } = node.props;
const props = { ...node.props.reactProps };
delete props.children;
const portalTarget = useStore(node.portalTarget);
const childrenSource = useStore(node.childrenSource);
const svelteChildren = useStore(node.svelteChildren);
const hooks = useStore(node.hooks);
const firstRender = React.useRef(true);
const { portalTarget, svelteChildren, childrenSource, hooks } = node;

if (svelteChildren) {
if (!children) {
Expand Down Expand Up @@ -58,8 +78,7 @@ const Bridge: React.FC<BridgeProps> = ({ node, createPortal }) => {
: React.createElement(node.reactComponent, props, children),
);
if (portalTarget && createPortal) {
if (firstRender.current) {
firstRender.current = false;
if (initialRender) {
portalTarget.innerHTML = ""; // Remove injected SSR content
}
return createPortal(vdom, portalTarget);
Expand All @@ -70,5 +89,5 @@ const Bridge: React.FC<BridgeProps> = ({ node, createPortal }) => {
{ node: node.key, style: { display: "none" } },
vdom,
);
};
}
export default Bridge;
50 changes: 25 additions & 25 deletions src/lib/internal/ReactWrapper.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,51 @@
* - Render a placeholder where the React component can portal into.
* - Render the Svelte children, that can be injected into the React component.
*/
import { writable } from "svelte/store";
import {
getAllContexts,
getContext,
onDestroy,
setContext,
type Snippet,
} from "svelte";
import type { FunctionComponent } from "react";
import type { FunctionComponent, ReactNode } from "react";
import type { SvelteInit, TreeNode } from "./types";
import deepRead from "./deepRead";
type Props = {
svelteInit: (options: SvelteInit) => TreeNode;
children?: Snippet;
react$Children?: unknown;
react$Children?: ReactNode;
};
let { svelteInit, children, react$Children, ...reactProps }: Props = $props();
const propsStore = writable<Record<string, any>>({
...reactProps,
children: react$Children,
});
const portalTarget = writable<HTMLElement | undefined>();
const svelteChildren = writable<Snippet | undefined>(children);
const childrenSource = writable<HTMLElement | undefined>();
const hooks = writable<Array<{ Hook: FunctionComponent; key: number }>>([]);
let portalTarget = $state<HTMLElement | undefined>();
$effect(() => {
propsStore.set({ ...reactProps, children: react$Children });
});
$effect(() => {
svelteChildren.set(children);
});
let childrenSource = $state<HTMLElement | undefined>();
let hooks = $state<Array<{ Hook: FunctionComponent; key: number }>>([]);
const parent = getContext<TreeNode | undefined>("ReactWrapper");
const node = setContext(
"ReactWrapper",
svelteInit({
parent,
props: propsStore,
portalTarget,
childrenSource,
svelteChildren,
hooks,
get props() {
return {
reactProps,
children: react$Children as ReactNode,
};
},
get portalTarget() {
return portalTarget;
},
get childrenSource() {
return childrenSource;
},
get svelteChildren() {
return children;
},
get hooks() {
return hooks;
},
context: getAllContexts(),
}),
);
Expand All @@ -64,13 +64,13 @@
<svelte-portal-target
node={node.key}
style="display:contents"
bind:this={$portalTarget}
bind:this={portalTarget}
></svelte-portal-target>

{#if children}
<svelte-children-source
node={node.key}
style="display:none"
bind:this={$childrenSource}>{@render children()}</svelte-children-source
bind:this={childrenSource}>{@render children()}</svelte-children-source
>
{/if}
15 changes: 6 additions & 9 deletions src/lib/internal/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { ComponentClass, FunctionComponent } from "react";
import type { Readable, Writable } from "svelte/store";
import type { ComponentClass, FunctionComponent, ReactNode } from "react";
import type { Snippet } from "svelte";
import type { BridgeProps } from "./Bridge";

export type HandlerName<T extends string> = `on${Capitalize<T>}`;
export type EventName<T extends string> = T extends `on${infer N}`
Expand Down Expand Up @@ -54,17 +52,16 @@ export type TreeNode = SvelteInit & {
reactComponent: FunctionComponent<any> | ComponentClass<any>;
key: string;
autoKey: number;
hooks: Writable<Array<{ Hook: FunctionComponent; key: number }>>;
nodes: TreeNode[];
rerender?: () => void;
};

export type SvelteInit = {
props: Readable<Record<string, any>>; // The react props
portalTarget: Readable<HTMLElement | undefined>; // An element to portal the React component into
childrenSource: Readable<HTMLElement | undefined>; // An element containing the children from Svelte, inject as children into the React component
svelteChildren: Readable<Snippet | undefined>; // The svelte children prop (snippet/slot)
props: { reactProps: Record<string, any>; children: ReactNode }; // The react props
portalTarget: HTMLElement | undefined; // An element to portal the React component into
childrenSource: HTMLElement | undefined; // An element containing the children from Svelte, inject as children into the React component
svelteChildren: Snippet | undefined; // The svelte children prop (snippet/slot)
context: Map<any, any>; // The full Svelte context
hooks: Writable<Array<{ Hook: FunctionComponent; key: number }>>;
hooks: Array<{ Hook: FunctionComponent; key: number }>;
parent?: TreeNode;
};
48 changes: 22 additions & 26 deletions src/lib/sveltify.ts → src/lib/sveltify.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as React from "react";
import type ReactDOMServer from "react-dom/server";
import { writable, get } from "svelte/store";
import type { SvelteInit, TreeNode } from "./internal/types";
import type { SvelteInit, TreeNode } from "./internal/types.js";
import ReactWrapper from "./internal/ReactWrapper.svelte";
import Bridge, { type BridgeProps } from "./internal/Bridge.js";
import { setPayload } from "./reactify";
import Bridge, { type BridgeProps } from "./internal/Bridge.svelte.js";
import { setPayload } from "./reactify.js";

let sharedRoot: TreeNode | undefined;

Expand All @@ -24,27 +23,34 @@ export default function sveltify<P>(
// eslint-disable-next-line no-param-reassign
$$props.svelteInit = (init: SvelteInit) => {
if (!init.parent && !sharedRoot) {
const portalTarget = writable<HTMLElement>();
let portalTarget = $state<HTMLElement | undefined>();
const hooks = $state<
Array<{ Hook: React.FunctionComponent; key: number }>
>([]);

const rootNode: TreeNode = {
key: anchorOrPayload.anchor ? `${anchorOrPayload.anchor}/` : "/",
autoKey: 0,
reactComponent: ({ children }: any) => children as React.ReactNode,
portalTarget,
props: writable({}),
childrenSource: writable(),
svelteChildren: writable(),
get portalTarget() {
return portalTarget;
},
props: { reactProps: {}, children: null },
childrenSource: undefined,
svelteChildren: undefined,
nodes: [],
context: new Map(),
hooks: writable([]),
get hooks() {
return hooks;
},
};
sharedRoot = rootNode;
if (client) {
const rootEl = document.createElement("react-root");
const root = ReactDOMClient.createRoot?.(rootEl);
const targetEl = document.createElement("bridge-root");
portalTarget.set(targetEl);
portalTarget = document.createElement("bridge-root");
document.head.appendChild(rootEl);
document.head.appendChild(targetEl);
document.head.appendChild(portalTarget);

if (root) {
rootNode.rerender = () => {
Expand All @@ -65,19 +71,13 @@ export default function sveltify<P>(
const parent = init.parent ?? (sharedRoot as TreeNode);
parent.autoKey += 1;
const key = `${parent.key}${parent.autoKey}/`;
const node: TreeNode = {
const node: TreeNode = Object.assign(init, {
key,
autoKey: 0,
reactComponent,
props: init.props,
childrenSource: init.childrenSource,
portalTarget: init.portalTarget,
svelteChildren: init.svelteChildren,
hooks: init.hooks,
context: init.context,
nodes: [],
rerender: parent.rerender,
};
});
parent.nodes.push(node);
if (client) {
parent.rerender?.();
Expand Down Expand Up @@ -123,11 +123,7 @@ function applyPortal(
node: TreeNode,
source: { html: string },
) {
const init = {
props: get(node.props),
svelteChildren: get(node.svelteChildren),
};
if (init.svelteChildren !== undefined) {
if (node.svelteChildren !== undefined) {
const child = extract(
`<svelte-children-source node="${node.key}" style="display:none">`,
`</svelte-children-source>`,
Expand Down

0 comments on commit e63fa2e

Please sign in to comment.