Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce react-utils package #627

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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/loud-walls-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@preact/signals-react": minor
---

Add a `@preact/signals-react/utils` entrypoint containing common Signal utilities
29 changes: 16 additions & 13 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ const pkgList = {
core: "@preact/signals-core",
preact: "@preact/signals",
react: "@preact/signals-react",
"react/utils": "@preact/signals-react/utils",
"react/auto": "@preact/signals-react/auto",
"react/runtime": "@preact/signals-react/runtime",
"react-transform": "@preact/signals-react-transform",
Expand Down Expand Up @@ -304,19 +305,21 @@ module.exports = function (config) {
customLaunchers: localLaunchers,

files: [
...filteredPkgList.some(i => /^react/.test(i)) ? [
{
// Provide some NodeJS globals to run babel in a browser environment
pattern: "test/browser/nodeGlobals.js",
watched: false,
type: "js",
},
{
pattern: "test/browser/babel.js",
watched: false,
type: "js",
},
] : [],
...(filteredPkgList.some(i => /^react/.test(i))
? [
{
// Provide some NodeJS globals to run babel in a browser environment
pattern: "test/browser/nodeGlobals.js",
watched: false,
type: "js",
},
{
pattern: "test/browser/babel.js",
watched: false,
type: "js",
},
]
: []),
{
pattern:
process.env.TESTS ||
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-auto && pnpm build:react && pnpm build:react-transform",
"build": "pnpm build:core && pnpm build:preact && pnpm build:react-runtime && pnpm build:react-auto && 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: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-auto": "pnpm _build --cwd packages/react/auto && pnpm postbuild:react-auto",
"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:react-utils": "cd packages/react/utils/dist && shx mv -f react/utils/src/index.d.ts . && shx rm -rf react",
"postbuild:react-auto": "cd packages/react/auto/dist && shx mv -f react/auto/src/*.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
9 changes: 8 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@
"import": "./auto/dist/auto.mjs",
"require": "./auto/dist/auto.js"
},
"./auto/package.json": "./auto/package.json"
"./auto/package.json": "./auto/package.json",
"./utils": {
"types": "./utils/dist/index.d.ts",
"browser": "./utils/dist/utils.module.js",
"import": "./utils/dist/utils.mjs",
"require": "./utils/dist/utils.js"
},
"./utils/package.json": "./utils/package.json"
},
"mangle": "../../mangle.json",
"files": [
Expand Down
26 changes: 26 additions & 0 deletions packages/react/utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@preact/signals-react-runtime",
"description": "Sub package for @preact/signals-react that contains some useful utilities",
"private": true,
"amdName": "reactSignalsutils",
"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": {
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
}
45 changes: 45 additions & 0 deletions packages/react/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ReadonlySignal, Signal } from "@preact/signals-core";
import { useSignal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
import { Fragment, createElement, useMemo } from "react";

interface ShowProps<T = boolean> {
when: Signal<T> | ReadonlySignal<T>;
fallback?: JSX.Element;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to switch to importing JSX, it's (finally!) no longer a global in React 19 AFAIK. It should exist on the React namespace back to 16.

children: JSX.Element | ((value: T) => JSX.Element);
}

export function Show<T = boolean>(props: ShowProps<T>): JSX.Element | null {
useSignals();
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>>;
fallback?: JSX.Element;
children: (value: T, index: number) => JSX.Element;
}

export function For<T>(props: ForProps<T>): JSX.Element | null {
useSignals();
const cache = useMemo(() => new Map(), []);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to switch this to a WeakMap when we see the first item being an object or leverage JSON.stringify on objects

const list = props.each.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, { children: items });
}

export function useLiveSignal<T>(value: Signal<T> | ReadonlySignal<T>) {
const s = useSignal(value);
if (s.peek() !== value) s.value = value;
return s;
}
69 changes: 69 additions & 0 deletions packages/react/utils/test/browser/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { For, Show } from "../../src";
import {
act,
checkHangingAct,
createRoot,
Root,
} from "../../../test/shared/utils";
import { signal } from "@preact/signals-react";
import { createElement } from "react";

describe("@preact/signals-react-utils", () => {
let scratch: HTMLDivElement;
let root: Root;
async function render(element: Parameters<Root["render"]>[0]) {
await act(() => root.render(element));
}

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

afterEach(async () => {
checkHangingAct();
await act(() => root.unmount());
scratch.remove();
});

describe("<Show />", () => {
it("Should reactively show an element", () => {
const toggle = signal(false)!;
const Paragraph = (p: any) => <p>{p.children}</p>;
act(() => {
render(
<Show when={toggle} fallback={<Paragraph>Hiding</Paragraph>}>
<Paragraph>Showing</Paragraph>
</Show>
);
});
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>
);
});
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>");
});
});
});
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@preact/signals-core": ["./packages/core/src/index.ts"],
"@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-react/runtime": [
"./packages/react/runtime/src/index.ts"
],
Expand Down
Loading