Skip to content

Commit ede3680

Browse files
bfangerBob Fanger
authored andcommitted
feat: Using React Hooks inside Svelte components
Fixes #14
1 parent 683fb92 commit ede3680

File tree

16 files changed

+259
-112
lines changed

16 files changed

+259
-112
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Supports:
99
- Nesting (Slot & Children)
1010
- Contexts
1111
- SSR
12+
- Hooks ([useStore](./docs/useStore.md) & [hooks](./docs/hooks.md))
1213

1314
# "Embrace, extend and extinguish"
1415

docs/hooks.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# hooks
2+
3+
Using React hooks inside Svelte components.
4+
Because React doesn't have an synchronous render anymore (by-design), the initial value of the store will be `undefined`.
5+
6+
The `hooks()` function uses Svelte lifecycle functions, so you can only call the function during component initialization.
7+
8+
### Usage:
9+
10+
```svelte
11+
<script lang="ts">
12+
import { hooks } from "$lib";
13+
14+
const store = hooks(() => useState(0));
15+
</script>
16+
17+
{#if $store}
18+
{@const [count, setCount] = $store}
19+
<h2>Count: {count}</h2>
20+
<button on:click={() => setCount(count + 1)}>+</button>
21+
{/if}
22+
```
23+
24+
What is returned from the hook becomes the value of the store, so to calling multiple hooks is fine, but [the rules of hooks](https://reactjs.org/docs/hooks-rules.html) still apply.
25+
26+
```ts
27+
const store = hooks(() => {
28+
const multiplier = useContext(MultiplierContext);
29+
const [count, setCount] = useState(0);
30+
return {
31+
multiply: () => setCount(count * multiplier),
32+
reset: () => setCount(0),
33+
};
34+
});
35+
36+
function onReset() {
37+
$store?.reset();
38+
}
39+
```

docs/reactify.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## reactify
2+
3+
Convert a Svelte component into an React component.
4+
5+
### Usage:
6+
7+
```ts
8+
import { reactify } from "svelte-preprocess-react";
9+
import ButtonSvelte from "$lib/components/Button.svelte";
10+
11+
const Button = reactify(ButtonSvelte);
12+
13+
type Props = {
14+
onClose: () => void;
15+
};
16+
const Dialog: React.FC<Props> = ({ onClose }) => (
17+
<div className="dialog">
18+
<h2>Thanks for subscribing!</h2>
19+
<Button type="primary" onClick={() => onClose()}>
20+
Close
21+
</Button>
22+
</div>
23+
);
24+
```
25+
26+
React only has props, we we asume that the props starting with "on" followed by a capital letter are event handlers.

docs/useStore.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## useStore
2+
3+
useStore is a React hook that allows using a Svelte Store in a React component.
4+
5+
### Usage:
6+
7+
```ts
8+
import { useStore } from "svelte-preprocess-react";
9+
const userStore = writable({ name: "John Doe" });
10+
11+
const UserGreet: React.FC = () => {
12+
const $user = useStore(userStore);
13+
return <h1>Hello, {$user.name}</h1>;
14+
};
15+
export default UserGreet;
16+
```
17+
18+
When the Svelte Store is updated the component will rerender and receive the new value from the useStore hook.
19+
20+
### Writable stores
21+
22+
Inside a Svelte component `$user = { name:'Jane Doe' }` or `$user.name = 'Jane Doe'` will trigger an update.
23+
In React and other regular javascript files this does _not_ work.
24+
To update the value and trigger an update use the `set` or `update` methods:
25+
26+
```ts
27+
// Instead of `$user = { name:'Jane Doe' }`
28+
user.set({ name: "Jane Doe" });
29+
30+
// Instead of `$user.name = 'Jane Doe'`
31+
user.update((user) => ({ ...user, name: "Jane Doe" }));
32+
```
33+
34+
See https://svelte.dev/docs#run-time-svelte-store for more information.

docs/utilities.md

Lines changed: 4 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,7 @@
11
# Utilities
22

3-
svelte-preprocess-react comes with a utilities to make using Svelte primitives inside React components easier.
4-
And to make using React primitives inside Svelte components easier.
3+
svelte-preprocess-react comes with utilities to make using Svelte primitives inside React components easier and vice versa.
54

6-
## reactify
7-
8-
Convert a Svelte component into an React component.
9-
10-
### Usage:
11-
12-
```ts
13-
import ButtonSvelte from "$lib/components/Button.svelte";
14-
const Button = reactify(ButtonSvelte);
15-
16-
type Props = {
17-
onClose: () => void;
18-
};
19-
const Dialog: React.FC<Props> = ({ onClose }) => (
20-
<div className="dialog">
21-
<h2>Thanks for subscribing!</h2>
22-
<Button type="primary" onClick={() => onClose()}>
23-
Close
24-
</Button>
25-
</div>
26-
);
27-
```
28-
29-
React only has props, we we asume that the props starting with "on" followed by a capital letter are event handlers.
30-
31-
## useStore
32-
33-
useStore is a React hook that allows using a Svelte Store in a React component.
34-
35-
### Usage:
36-
37-
```ts
38-
import { useStore } from "svelte-preprocess-react";
39-
const userStore = writable({ name: "John Doe" });
40-
41-
const UserGreet: React.FC = () => {
42-
const $user = useStore(userStore);
43-
return <h1>Hello, {$user.name}</h1>;
44-
};
45-
export default UserGreet;
46-
```
47-
48-
When the Svelte Store is updated the component will rerender and receive the new value from the useStore hook.
49-
50-
### Writable stores
51-
52-
Inside a Svelte component `$user = { name:'Jane Doe' }` or `$user.name = 'Jane Doe'` will trigger an update.
53-
In React and other regular javascript files this does _not_ work.
54-
To update the value and trigger an update use the `set` or `update` methods:
55-
56-
```ts
57-
// Instead of `$user = { name:'Jane Doe' }`
58-
user.set({ name: "Jane Doe" });
59-
60-
// Instead of `$user.name = 'Jane Doe'`
61-
user.update((user) => ({ ...user, name: "Jane Doe" }));
62-
```
63-
64-
See https://svelte.dev/docs#run-time-svelte-store for more information.
5+
- [reactify](./reactify.md) - Create a React component from a Svelte component
6+
- [useStore](./useStore.md) - Use a Svelte store in a React component
7+
- [hooks](./hooks.md) - Use React hooks in a Svelte component

src/lib/Hook.svelte

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

src/lib/hooks.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { createElement } from "react";
2+
import { writable, type Readable } from "svelte/store";
3+
import type ReactDOMServer from "react-dom/server";
4+
import { getContext, onDestroy } from "svelte";
5+
import type { TreeNode } from "./internal/types";
6+
7+
export default function hooks<T>(
8+
callback: () => T,
9+
ReactDOMClient?: any,
10+
renderToString?: typeof ReactDOMServer.renderToString
11+
): Readable<T | undefined> {
12+
const store = writable<T | undefined>();
13+
14+
const parent = getContext<TreeNode | undefined>("ReactWrapper");
15+
function Hook() {
16+
store.set(callback());
17+
return null;
18+
}
19+
20+
if (parent) {
21+
const hook = { Hook, key: getKey(parent) };
22+
parent.hooks.update(($hooks) => [...$hooks, hook]);
23+
onDestroy(() => {
24+
parent.hooks.update(($hooks) => $hooks.filter((entry) => entry !== hook));
25+
});
26+
} else if (ReactDOMClient) {
27+
onDestroy(standalone(Hook, ReactDOMClient, renderToString));
28+
} else if (typeof window !== "undefined") {
29+
throw new Error(
30+
"The ReactDOMClient parameter is required for hooks(), because no parent component was a sveltified React component"
31+
);
32+
}
33+
return store;
34+
}
35+
36+
function standalone(
37+
Hook: React.FC,
38+
ReactDOMClient: any,
39+
renderToString?: typeof ReactDOMServer.renderToString
40+
) {
41+
if (typeof document === "undefined") {
42+
if (!renderToString) {
43+
throw new Error("renderToString parameter is required for SSR");
44+
}
45+
renderToString(createElement(Hook));
46+
return () => {};
47+
}
48+
const el = document.createElement("react-hooks");
49+
const root = ReactDOMClient.createRoot?.(el);
50+
if (root) {
51+
root.render(createElement(Hook));
52+
} else {
53+
ReactDOMClient.render(createElement(Hook), el);
54+
}
55+
return () => {
56+
if (root) {
57+
root.unmount();
58+
} else {
59+
ReactDOMClient.unmountComponentAtNode(el);
60+
}
61+
};
62+
}
63+
64+
const autokeys = new WeakMap();
65+
function getKey(node: TreeNode | undefined) {
66+
if (!node) {
67+
return undefined;
68+
}
69+
let autokey = autokeys.get(node);
70+
if (autokey === undefined) {
71+
autokey = 0;
72+
} else {
73+
autokey += 1;
74+
}
75+
autokeys.set(node, autokey);
76+
return autokey;
77+
}

src/lib/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { default as useStore } from "./useStore.js";
22
export { default as reactify } from "./reactify.js";
33
export { default as sveltify } from "./sveltify.js";
4-
export { default as Hook } from "./Hook.svelte";
4+
export { default as used } from "./used.js";
5+
export { default as hooks } from "./hooks.js";

src/lib/internal/Bridge.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const Bridge: React.FC<BridgeProps> = ({ createPortal, node }) => {
1717
const target = useStore(node.target);
1818
let props = useStore(node.props);
1919
const slot = useStore(node.slot);
20+
const hooks = useStore(node.hooks);
2021

2122
if (!target) {
2223
return null;
@@ -36,6 +37,11 @@ const Bridge: React.FC<BridgeProps> = ({ createPortal, node }) => {
3637
if (slot) {
3738
children.push(createElement(Child, { key: "svelte-slot", el: slot }));
3839
}
40+
if (hooks.length >= 0) {
41+
children.push(
42+
...hooks.map(({ Hook, key }) => createElement(Hook, { key }))
43+
);
44+
}
3945
return createPortal(
4046
createElement(
4147
SvelteToReactContext.Provider,

src/lib/internal/ReactWrapper.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
import { writable } from "svelte/store";
33
import { beforeUpdate, getContext, onDestroy, setContext } from "svelte";
44
import type { SvelteInit, TreeNode } from "./types";
5+
import type React from "react";
56
67
export let svelteInit: (options: SvelteInit) => TreeNode;
78
89
const props = writable<Record<string, any>>(extractProps($$props));
910
const target = writable<HTMLElement | undefined>();
1011
const slot = writable<HTMLElement | undefined>();
12+
const hooks = writable<Array<{ Hook: React.FC; key: number }>>([]);
1113
const listeners: Array<() => void> = [];
1214
1315
const parent = getContext<TreeNode | undefined>("ReactWrapper");
@@ -17,6 +19,7 @@
1719
props,
1820
target,
1921
slot,
22+
hooks,
2023
onDestroy(callback) {
2124
listeners.push(callback);
2225
},

src/lib/internal/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import type React from "react";
12
import type { ComponentClass, FunctionComponent } from "react";
2-
import type { Readable } from "svelte/store";
3+
import type { Readable, Writable } from "svelte/store";
34

45
export type ConstructorOf<T> = {
56
new (): T;
@@ -57,7 +58,8 @@ export type OmitEventProps<ReactProps> = Omit<
5758
export type TreeNode = Omit<SvelteInit, "onDestroy"> & {
5859
svelteInstance: Readable<any>;
5960
reactComponent: FunctionComponent<any> | ComponentClass<any>;
60-
key: string;
61+
key: number;
62+
hooks: Writable<Array<{ Hook: React.FC; key: number }>>;
6163
nodes: TreeNode[];
6264
};
6365

@@ -66,5 +68,6 @@ export type SvelteInit = {
6668
props: Readable<Record<string, any>>;
6769
target: Readable<HTMLElement | undefined>;
6870
slot: Readable<HTMLElement | undefined>;
71+
hooks: Writable<Array<{ Hook: React.FC; key: number }>>;
6972
onDestroy: (callback: () => void) => void;
7073
};

src/lib/sveltify.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ let autokey = 0;
1313
const never = writable() as Readable<any>;
1414
const target = writable<HTMLElement>();
1515
const tree: TreeNode = {
16-
key: "root",
16+
key: autokey,
1717
svelteInstance: never,
1818
reactComponent: ({ children }: any) => children,
1919
target,
2020
props: writable({}),
2121
slot: never,
2222
nodes: [],
23+
hooks: writable([]),
2324
};
2425

2526
type Sveltified<P> = ConstructorOf<SvelteComponentTyped<Omit<P, "children">>>;
@@ -88,12 +89,13 @@ export default function sveltify<P>(
8889
svelteInit(init: SvelteInit) {
8990
autokey += 1;
9091
const node = {
91-
key: autokey.toString(),
92+
key: autokey,
9293
svelteInstance,
9394
reactComponent,
9495
props: init.props,
9596
slot: init.slot,
9697
target: init.target,
98+
hooks: init.hooks,
9799
nodes: [],
98100
};
99101
const parent = init.parent ?? tree;

src/lib/used.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* eslint-disable @typescript-eslint/no-unused-vars */
22

33
/**
4+
* This method does absolutely nothing, it's a no-op
5+
*
46
* Svelte/TypeScript is not (yet) able to detect usage of the <react:ComponentX> syntax.
57
* This causes `X is declared but its value is never read. (ts:6133)` errors.
68
*

0 commit comments

Comments
 (0)