Skip to content

Commit 4b585a5

Browse files
Offer Preact utils as well (#683)
* Offer Preact utils as well * Delete packages/preact/src/utils.ts * Suggestions * Update packages/preact/utils/package.json Co-authored-by: Ryan Christian <[email protected]> * Correctly inherit peerDep * Add parent --------- Co-authored-by: Ryan Christian <[email protected]>
1 parent acfa71c commit 4b585a5

File tree

12 files changed

+286
-4
lines changed

12 files changed

+286
-4
lines changed

.changeset/fast-pens-leave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals": minor
3+
---
4+
5+
Provide `@preact/signals/utils` package with some helpers to make working with signals easier in Preact

karma.conf.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ const pkgList = {
227227
"react/utils": "@preact/signals-react/utils",
228228
"react/runtime": "@preact/signals-react/runtime",
229229
"react-transform": "@preact/signals-react-transform",
230+
"preact/utils": "@preact/signals/utils",
230231
};
231232

232233
module.exports = function (config) {

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
"private": true,
44
"scripts": {
55
"prebuild": "shx rm -rf packages/*/dist/",
6-
"build": "pnpm build:core && pnpm build:preact && pnpm build:react-runtime && pnpm build:react && pnpm build:react-transform && pnpm build:react-utils",
6+
"build": "pnpm build:core && pnpm build:preact && pnpm build:preact-utils && pnpm build:react-runtime && pnpm build:react && pnpm build:react-transform && pnpm build:react-utils",
77
"_build": "microbundle --raw --globals @preact/signals-core=preactSignalsCore,preact/hooks=preactHooks,@preact/signals-react/runtime=reactSignalsRuntime",
88
"build:core": "pnpm _build --cwd packages/core && pnpm postbuild:core",
99
"build:preact": "pnpm _build --cwd packages/preact && pnpm postbuild:preact",
10+
"build:preact-utils": "pnpm _build --cwd packages/preact/utils && pnpm postbuild:preact-utils",
1011
"build:react": "pnpm _build --cwd packages/react --external \"react,@preact/signals-react/runtime,@preact/signals-core\" && pnpm postbuild:react",
1112
"build:react-utils": "pnpm _build --cwd packages/react/utils && pnpm postbuild:react-utils",
1213
"build:react-runtime": "pnpm _build --cwd packages/react/runtime && pnpm postbuild:react-runtime",
1314
"build:react-transform": "pnpm _build --no-compress --cwd packages/react-transform",
1415
"postbuild:core": "cd packages/core/dist && shx mv -f index.d.ts signals-core.d.ts",
1516
"postbuild:preact": "cd packages/preact/dist && shx mv -f preact/src/index.d.ts signals.d.ts && shx rm -rf preact",
1617
"postbuild:react": "cd packages/react/dist && shx mv -f react/src/index.d.ts signals.d.ts && shx rm -rf react",
18+
"postbuild:preact-utils": "cd packages/preact/utils/dist && shx mv -f preact/utils/src/index.d.ts . && shx rm -rf preact",
1719
"postbuild:react-utils": "cd packages/react/utils/dist && shx mv -f react/utils/src/index.d.ts . && shx rm -rf react",
1820
"postbuild:react-runtime": "cd packages/react/runtime/dist && shx mv -f react/runtime/src/*.d.ts . && shx rm -rf react",
1921
"lint": "pnpm lint:eslint && pnpm lint:tsc",

packages/preact/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,84 @@ function Person() {
111111

112112
This way we'll bypass checking the virtual-dom and update the DOM property directly.
113113

114+
## Utility Components and Hooks
115+
116+
The `@preact/signals/utils` package provides additional utility components and hooks to make working with signals even easier.
117+
118+
### Show Component
119+
120+
The `Show` component provides a declarative way to conditionally render content based on a signal's value.
121+
122+
```js
123+
import { Show } from "@preact/signals/utils";
124+
import { signal } from "@preact/signals";
125+
126+
const isVisible = signal(false);
127+
128+
function App() {
129+
return (
130+
<Show when={isVisible} fallback={<p>Nothing to see here</p>}>
131+
<p>Now you see me!</p>
132+
</Show>
133+
);
134+
}
135+
136+
// You can also use a function to access the value
137+
function App() {
138+
return <Show when={isVisible}>{value => <p>The value is {value}</p>}</Show>;
139+
}
140+
```
141+
142+
### For Component
143+
144+
The `For` component helps you render lists from signal arrays with automatic caching of rendered items.
145+
146+
```js
147+
import { For } from "@preact/signals/utils";
148+
import { signal } from "@preact/signals";
149+
150+
const items = signal(["A", "B", "C"]);
151+
152+
function App() {
153+
return (
154+
<For each={items} fallback={<p>No items</p>}>
155+
{(item, index) => <div key={index}>Item: {item}</div>}
156+
</For>
157+
);
158+
}
159+
```
160+
161+
### Additional Hooks
162+
163+
#### useLiveSignal
164+
165+
The `useLiveSignal` hook allows you to create a local signal that stays synchronized with an external signal.
166+
167+
```js
168+
import { useLiveSignal } from "@preact/signals/utils";
169+
import { signal } from "@preact/signals";
170+
171+
const external = signal(0);
172+
173+
function Component() {
174+
const local = useLiveSignal(external);
175+
// local will automatically update when external changes
176+
}
177+
```
178+
179+
#### useSignalRef
180+
181+
The `useSignalRef` hook creates a signal that behaves like a React ref with a `.current` property.
182+
183+
```js
184+
import { useSignalRef } from "@preact/signals/utils";
185+
186+
function Component() {
187+
const ref = useSignalRef(null);
188+
return <div ref={ref}>The ref's value is {ref.current}</div>;
189+
}
190+
```
191+
114192
## License
115193
116194
`MIT`, see the [LICENSE](../../LICENSE) file.

packages/preact/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
"browser": "./dist/signals.module.js",
3131
"import": "./dist/signals.mjs",
3232
"require": "./dist/signals.js"
33+
},
34+
"./utils": {
35+
"types": "./utils/dist/index.d.ts",
36+
"browser": "./utils/dist/utils.module.js",
37+
"import": "./utils/dist/utils.mjs",
38+
"require": "./utils/dist/utils.js"
3339
}
3440
},
3541
"mangle": "../../mangle.json",
@@ -38,7 +44,10 @@
3844
"dist",
3945
"CHANGELOG.md",
4046
"LICENSE",
41-
"README.md"
47+
"README.md",
48+
"utils/dist",
49+
"utils/package.json",
50+
"utils/src"
4251
],
4352
"scripts": {
4453
"prepublishOnly": "cd ../.. && pnpm build:preact"

packages/preact/utils/package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@preact/signals-utils",
3+
"description": "Sub package for @preact/signals that contains some useful utilities",
4+
"private": true,
5+
"amdName": "preactSignalsutils",
6+
"main": "dist/utils.js",
7+
"module": "dist/utils.module.js",
8+
"unpkg": "dist/utils.min.js",
9+
"types": "dist/index.d.ts",
10+
"source": "src/index.ts",
11+
"mangle": "../../../mangle.json",
12+
"exports": {
13+
".": {
14+
"types": "./dist/index.d.ts",
15+
"browser": "./dist/utils.module.js",
16+
"import": "./dist/utils.mjs",
17+
"require": "./dist/utils.js"
18+
}
19+
},
20+
"dependencies": {
21+
"@preact/signals-core": "workspace:^1.3.0"
22+
},
23+
"peerDependencies": {
24+
"@preact/signals": "workspace:*",
25+
"preact": ">= 10.25.0"
26+
}
27+
}

packages/preact/utils/src/index.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { ReadonlySignal, Signal } from "@preact/signals-core";
2+
import { useSignal } from "@preact/signals";
3+
import { Fragment, createElement, JSX } from "preact";
4+
import { useMemo } from "preact/hooks";
5+
6+
interface ShowProps<T = boolean> {
7+
when: Signal<T> | ReadonlySignal<T>;
8+
fallback?: JSX.Element;
9+
children: JSX.Element | ((value: T) => JSX.Element);
10+
}
11+
12+
export function Show<T = boolean>(props: ShowProps<T>): JSX.Element | null {
13+
const value = props.when.value;
14+
if (!value) return props.fallback || null;
15+
return typeof props.children === "function"
16+
? props.children(value)
17+
: props.children;
18+
}
19+
20+
interface ForProps<T> {
21+
each:
22+
| Signal<Array<T>>
23+
| ReadonlySignal<Array<T>>
24+
| (() => Signal<Array<T>> | ReadonlySignal<Array<T>>);
25+
fallback?: JSX.Element;
26+
children: (value: T, index: number) => JSX.Element;
27+
}
28+
29+
export function For<T>(props: ForProps<T>): JSX.Element | null {
30+
const cache = useMemo(() => new Map(), []);
31+
let list = (
32+
(typeof props.each === "function" ? props.each() : props.each) as Signal<
33+
Array<T>
34+
>
35+
).value;
36+
37+
if (!list.length) return props.fallback || null;
38+
39+
const items = list.map((value, key) => {
40+
if (!cache.has(value)) {
41+
cache.set(value, props.children(value, key));
42+
}
43+
return cache.get(value);
44+
});
45+
46+
return createElement(Fragment, null, items);
47+
}
48+
49+
export function useLiveSignal<T>(
50+
value: Signal<T> | ReadonlySignal<T>
51+
): Signal<Signal<T> | ReadonlySignal<T>> {
52+
const s = useSignal(value);
53+
if (s.peek() !== value) s.value = value;
54+
return s;
55+
}
56+
57+
export function useSignalRef<T>(value: T): Signal<T> & { current: T } {
58+
const ref = useSignal(value) as Signal<T> & { current: T };
59+
if (!("current" in ref))
60+
Object.defineProperty(ref, "current", refSignalProto);
61+
return ref;
62+
}
63+
const refSignalProto = {
64+
configurable: true,
65+
get(this: Signal) {
66+
return this.value;
67+
},
68+
set(this: Signal, v: any) {
69+
this.value = v;
70+
},
71+
};
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { signal } from "@preact/signals";
2+
import { For, Show, useSignalRef } from "@preact/signals/utils";
3+
import { render, createElement } from "preact";
4+
import { act } from "preact/test-utils";
5+
6+
describe("@preact/signals-utils", () => {
7+
let scratch: HTMLDivElement;
8+
9+
beforeEach(async () => {
10+
scratch = document.createElement("div");
11+
document.body.appendChild(scratch);
12+
});
13+
14+
afterEach(async () => {
15+
render(null, scratch);
16+
});
17+
18+
describe("<Show />", () => {
19+
it("Should reactively show an element", () => {
20+
const toggle = signal(false)!;
21+
const Paragraph = (props: any) => <p>{props.children}</p>;
22+
act(() => {
23+
render(
24+
<Show when={toggle} fallback={<Paragraph>Hiding</Paragraph>}>
25+
<Paragraph>Showing</Paragraph>
26+
</Show>,
27+
scratch
28+
);
29+
});
30+
expect(scratch.innerHTML).to.eq("<p>Hiding</p>");
31+
32+
act(() => {
33+
toggle.value = true;
34+
});
35+
expect(scratch.innerHTML).to.eq("<p>Showing</p>");
36+
});
37+
});
38+
39+
describe("<For />", () => {
40+
it("Should iterate over a list of signals", () => {
41+
const list = signal<Array<string>>([])!;
42+
const Paragraph = (p: any) => <p>{p.children}</p>;
43+
act(() => {
44+
render(
45+
<For each={list} fallback={<Paragraph>No items</Paragraph>}>
46+
{item => <Paragraph key={item}>{item}</Paragraph>}
47+
</For>,
48+
scratch
49+
);
50+
});
51+
expect(scratch.innerHTML).to.eq("<p>No items</p>");
52+
53+
act(() => {
54+
list.value = ["foo", "bar"];
55+
});
56+
expect(scratch.innerHTML).to.eq("<p>foo</p><p>bar</p>");
57+
});
58+
});
59+
60+
describe("useSignalRef", () => {
61+
it("should work", () => {
62+
let ref;
63+
const Paragraph = (p: any) => {
64+
ref = useSignalRef(null);
65+
return p.type === "span" ? (
66+
<span ref={ref}>{p.children}</span>
67+
) : (
68+
<p ref={ref}>{p.children}</p>
69+
);
70+
};
71+
act(() => {
72+
render(<Paragraph type="p">1</Paragraph>, scratch);
73+
});
74+
expect(scratch.innerHTML).to.eq("<p>1</p>");
75+
expect((ref as any).value instanceof HTMLParagraphElement).to.eq(true);
76+
77+
act(() => {
78+
render(<Paragraph type="span">1</Paragraph>, scratch);
79+
});
80+
expect(scratch.innerHTML).to.eq("<span>1</span>");
81+
expect((ref as any).value instanceof HTMLSpanElement).to.eq(true);
82+
});
83+
});
84+
});

packages/react/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
"runtime/dist",
5454
"runtime/src",
5555
"runtime/package.json",
56+
"utils/dist",
57+
"utils/src",
58+
"utils/package.json",
5659
"CHANGELOG.md",
5760
"LICENSE",
5861
"README.md"

packages/react/utils/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@preact/signals-react-runtime",
2+
"name": "@preact/signals-react-utils",
33
"description": "Sub package for @preact/signals-react that contains some useful utilities",
44
"private": true,
55
"amdName": "reactSignalsutils",
@@ -21,6 +21,7 @@
2121
"@preact/signals-core": "workspace:^1.3.0"
2222
},
2323
"peerDependencies": {
24+
"@preact/signals-react": "workspace:*",
2425
"react": "^16.14.0 || 17.x || 18.x || 19.x"
2526
}
2627
}

0 commit comments

Comments
 (0)