Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-pens-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@preact/signals": minor
---

Provide `@preact/signals/utils` package with some helpers to make working with signals easier in Preact
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ const pkgList = {
"react/utils": "@preact/signals-react/utils",
"react/runtime": "@preact/signals-react/runtime",
"react-transform": "@preact/signals-react-transform",
"preact/utils": "@preact/signals/utils",
};

module.exports = function (config) {
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
"private": true,
"scripts": {
"prebuild": "shx rm -rf packages/*/dist/",
"build": "pnpm build:core && pnpm build:preact && pnpm build:react-runtime && pnpm build:react && pnpm build:react-transform && pnpm build:react-utils",
"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",
"_build": "microbundle --raw --globals @preact/signals-core=preactSignalsCore,preact/hooks=preactHooks,@preact/signals-react/runtime=reactSignalsRuntime",
"build:core": "pnpm _build --cwd packages/core && pnpm postbuild:core",
"build:preact": "pnpm _build --cwd packages/preact && pnpm postbuild:preact",
"build:preact-utils": "pnpm _build --cwd packages/preact/utils && pnpm postbuild:preact-utils",
"build:react": "pnpm _build --cwd packages/react --external \"react,@preact/signals-react/runtime,@preact/signals-core\" && pnpm postbuild:react",
"build:react-utils": "pnpm _build --cwd packages/react/utils && pnpm postbuild:react-utils",
"build:react-runtime": "pnpm _build --cwd packages/react/runtime && pnpm postbuild:react-runtime",
"build:react-transform": "pnpm _build --no-compress --cwd packages/react-transform",
"postbuild:core": "cd packages/core/dist && shx mv -f index.d.ts signals-core.d.ts",
"postbuild:preact": "cd packages/preact/dist && shx mv -f preact/src/index.d.ts signals.d.ts && shx rm -rf preact",
"postbuild:react": "cd packages/react/dist && shx mv -f react/src/index.d.ts signals.d.ts && shx rm -rf react",
"postbuild:preact-utils": "cd packages/preact/utils/dist && shx mv -f preact/utils/src/index.d.ts . && shx rm -rf preact",
"postbuild:react-utils": "cd packages/react/utils/dist && shx mv -f react/utils/src/index.d.ts . && shx rm -rf react",
"postbuild:react-runtime": "cd packages/react/runtime/dist && shx mv -f react/runtime/src/*.d.ts . && shx rm -rf react",
"lint": "pnpm lint:eslint && pnpm lint:tsc",
Expand Down
78 changes: 78 additions & 0 deletions packages/preact/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,84 @@ function Person() {

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

## Utility Components and Hooks

The `@preact/signals/utils` package provides additional utility components and hooks to make working with signals even easier.

### Show Component

The `Show` component provides a declarative way to conditionally render content based on a signal's value.

```js
import { Show } from "@preact/signals/utils";
import { signal } from "@preact/signals";

const isVisible = signal(false);

function App() {
return (
<Show when={isVisible} fallback={<p>Nothing to see here</p>}>
<p>Now you see me!</p>
</Show>
);
}

// You can also use a function to access the value
function App() {
return <Show when={isVisible}>{value => <p>The value is {value}</p>}</Show>;
}
```

### For Component

The `For` component helps you render lists from signal arrays with automatic caching of rendered items.

```js
import { For } from "@preact/signals/utils";
import { signal } from "@preact/signals";

const items = signal(["A", "B", "C"]);

function App() {
return (
<For each={items} fallback={<p>No items</p>}>
{(item, index) => <div key={index}>Item: {item}</div>}
</For>
);
}
```

### Additional Hooks

#### useLiveSignal

The `useLiveSignal` hook allows you to create a local signal that stays synchronized with an external signal.

```js
import { useLiveSignal } from "@preact/signals/utils";
import { signal } from "@preact/signals";

const external = signal(0);

function Component() {
const local = useLiveSignal(external);
// local will automatically update when external changes
}
```

#### useSignalRef

The `useSignalRef` hook creates a signal that behaves like a React ref with a `.current` property.

```js
import { useSignalRef } from "@preact/signals/utils";

function Component() {
const ref = useSignalRef(null);
return <div ref={ref}>The ref's value is {ref.current}</div>;
}
```

## License

`MIT`, see the [LICENSE](../../LICENSE) file.
11 changes: 10 additions & 1 deletion packages/preact/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
"browser": "./dist/signals.module.js",
"import": "./dist/signals.mjs",
"require": "./dist/signals.js"
},
"./utils": {
"types": "./utils/dist/index.d.ts",
"browser": "./utils/dist/utils.module.js",
"import": "./utils/dist/utils.mjs",
"require": "./utils/dist/utils.js"
}
},
"mangle": "../../mangle.json",
Expand All @@ -38,7 +44,10 @@
"dist",
"CHANGELOG.md",
"LICENSE",
"README.md"
"README.md",
"utils/dist",
"utils/package.json",
"utils/src"
],
"scripts": {
"prepublishOnly": "cd ../.. && pnpm build:preact"
Expand Down
27 changes: 27 additions & 0 deletions packages/preact/utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@preact/signals-utils",
"description": "Sub package for @preact/signals that contains some useful utilities",
"private": true,
"amdName": "preactSignalsutils",
"main": "dist/utils.js",
"module": "dist/utils.module.js",
"unpkg": "dist/utils.min.js",
"types": "dist/index.d.ts",
"source": "src/index.ts",
"mangle": "../../../mangle.json",
"exports": {
".": {
"types": "./dist/index.d.ts",
"browser": "./dist/utils.module.js",
"import": "./dist/utils.mjs",
"require": "./dist/utils.js"
}
},
"dependencies": {
"@preact/signals-core": "workspace:^1.3.0"
},
"peerDependencies": {
"@preact/signals": "workspace:*",
"preact": ">= 10.25.0"
}
}
71 changes: 71 additions & 0 deletions packages/preact/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ReadonlySignal, Signal } from "@preact/signals-core";
import { useSignal } from "@preact/signals";
import { Fragment, createElement, JSX } from "preact";
import { useMemo } from "preact/hooks";

interface ShowProps<T = boolean> {
when: Signal<T> | ReadonlySignal<T>;
fallback?: JSX.Element;
children: JSX.Element | ((value: T) => JSX.Element);
}

export function Show<T = boolean>(props: ShowProps<T>): JSX.Element | null {
const value = props.when.value;
if (!value) return props.fallback || null;
return typeof props.children === "function"
? props.children(value)
: props.children;
}

interface ForProps<T> {
each:
| Signal<Array<T>>
| ReadonlySignal<Array<T>>
| (() => Signal<Array<T>> | ReadonlySignal<Array<T>>);
fallback?: JSX.Element;
children: (value: T, index: number) => JSX.Element;
}

export function For<T>(props: ForProps<T>): JSX.Element | null {
const cache = useMemo(() => new Map(), []);
let list = (
(typeof props.each === "function" ? props.each() : props.each) as Signal<
Array<T>
>
).value;

if (!list.length) return props.fallback || null;

const items = list.map((value, key) => {
if (!cache.has(value)) {
cache.set(value, props.children(value, key));
}
return cache.get(value);
});

return createElement(Fragment, null, items);
}

export function useLiveSignal<T>(
value: Signal<T> | ReadonlySignal<T>
): Signal<Signal<T> | ReadonlySignal<T>> {
const s = useSignal(value);
if (s.peek() !== value) s.value = value;
return s;
}

export function useSignalRef<T>(value: T): Signal<T> & { current: T } {
const ref = useSignal(value) as Signal<T> & { current: T };
if (!("current" in ref))
Object.defineProperty(ref, "current", refSignalProto);
return ref;
}
const refSignalProto = {
configurable: true,
get(this: Signal) {
return this.value;
},
set(this: Signal, v: any) {
this.value = v;
},
};
84 changes: 84 additions & 0 deletions packages/preact/utils/test/browser/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { signal } from "@preact/signals";
import { For, Show, useSignalRef } from "@preact/signals/utils";
import { render, createElement } from "preact";
import { act } from "preact/test-utils";

describe("@preact/signals-utils", () => {
let scratch: HTMLDivElement;

beforeEach(async () => {
scratch = document.createElement("div");
document.body.appendChild(scratch);
});

afterEach(async () => {
render(null, scratch);
});

describe("<Show />", () => {
it("Should reactively show an element", () => {
const toggle = signal(false)!;
const Paragraph = (props: any) => <p>{props.children}</p>;
act(() => {
render(
<Show when={toggle} fallback={<Paragraph>Hiding</Paragraph>}>
<Paragraph>Showing</Paragraph>
</Show>,
scratch
);
});
expect(scratch.innerHTML).to.eq("<p>Hiding</p>");

act(() => {
toggle.value = true;
});
expect(scratch.innerHTML).to.eq("<p>Showing</p>");
});
});

describe("<For />", () => {
it("Should iterate over a list of signals", () => {
const list = signal<Array<string>>([])!;
const Paragraph = (p: any) => <p>{p.children}</p>;
act(() => {
render(
<For each={list} fallback={<Paragraph>No items</Paragraph>}>
{item => <Paragraph key={item}>{item}</Paragraph>}
</For>,
scratch
);
});
expect(scratch.innerHTML).to.eq("<p>No items</p>");

act(() => {
list.value = ["foo", "bar"];
});
expect(scratch.innerHTML).to.eq("<p>foo</p><p>bar</p>");
});
});

describe("useSignalRef", () => {
it("should work", () => {
let ref;
const Paragraph = (p: any) => {
ref = useSignalRef(null);
return p.type === "span" ? (
<span ref={ref}>{p.children}</span>
) : (
<p ref={ref}>{p.children}</p>
);
};
act(() => {
render(<Paragraph type="p">1</Paragraph>, scratch);
});
expect(scratch.innerHTML).to.eq("<p>1</p>");
expect((ref as any).value instanceof HTMLParagraphElement).to.eq(true);

act(() => {
render(<Paragraph type="span">1</Paragraph>, scratch);
});
expect(scratch.innerHTML).to.eq("<span>1</span>");
expect((ref as any).value instanceof HTMLSpanElement).to.eq(true);
});
});
});
3 changes: 3 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
"runtime/dist",
"runtime/src",
"runtime/package.json",
"utils/dist",
"utils/src",
"utils/package.json",
"CHANGELOG.md",
"LICENSE",
"README.md"
Expand Down
3 changes: 2 additions & 1 deletion packages/react/utils/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@preact/signals-react-runtime",
"name": "@preact/signals-react-utils",
"description": "Sub package for @preact/signals-react that contains some useful utilities",
"private": true,
"amdName": "reactSignalsutils",
Expand All @@ -21,6 +21,7 @@
"@preact/signals-core": "workspace:^1.3.0"
},
"peerDependencies": {
"@preact/signals-react": "workspace:*",
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
}
2 changes: 1 addition & 1 deletion packages/react/utils/test/browser/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { For, Show, useSignalRef } from "../../src";
import { For, Show, useSignalRef } from "@preact/signals-react/utils";
import {
act,
checkHangingAct,
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@preact/signals": ["./packages/preact/src/index.ts"],
"@preact/signals-react": ["./packages/react/src/index.ts"],
"@preact/signals-react/utils": ["./packages/react/utils/src/index.ts"],
"@preact/signals/utils": ["./packages/preact/utils/src/index.ts"],
"@preact/signals-react/runtime": [
"./packages/react/runtime/src/index.ts"
],
Expand Down