From 0df78b9742387aee8e0bbc5eb0c80bc4eb4ef300 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Tue, 17 Dec 2024 20:45:12 +0100 Subject: [PATCH] Add react-utils package --- karma.conf.js | 29 ++++---- package.json | 4 +- packages/react/package.json | 9 ++- packages/react/utils/package.json | 26 +++++++ packages/react/utils/src/index.ts | 45 ++++++++++++ .../react/utils/test/browser/index.test.tsx | 69 +++++++++++++++++++ tsconfig.json | 1 + 7 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 packages/react/utils/package.json create mode 100644 packages/react/utils/src/index.ts create mode 100644 packages/react/utils/test/browser/index.test.tsx diff --git a/karma.conf.js b/karma.conf.js index f7688ff3..761277d6 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -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", @@ -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 || diff --git a/package.json b/package.json index 84d96d55..955cc992 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/react/package.json b/packages/react/package.json index 65bd12ac..d73a52ea 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -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": [ diff --git a/packages/react/utils/package.json b/packages/react/utils/package.json new file mode 100644 index 00000000..94b68c66 --- /dev/null +++ b/packages/react/utils/package.json @@ -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" + } +} diff --git a/packages/react/utils/src/index.ts b/packages/react/utils/src/index.ts new file mode 100644 index 00000000..c1861847 --- /dev/null +++ b/packages/react/utils/src/index.ts @@ -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 { + when: Signal | ReadonlySignal; + fallback?: JSX.Element; + children: JSX.Element | ((value: T) => JSX.Element); +} + +export function Show(props: ShowProps): 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 { + each: Signal> | ReadonlySignal>; + fallback?: JSX.Element; + children: (value: T, index: number) => JSX.Element; +} + +export function For(props: ForProps): JSX.Element | null { + useSignals(); + const cache = useMemo(() => new Map(), []); + 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(value: Signal | ReadonlySignal) { + const s = useSignal(value); + if (s.peek() !== value) s.value = value; + return s; +} diff --git a/packages/react/utils/test/browser/index.test.tsx b/packages/react/utils/test/browser/index.test.tsx new file mode 100644 index 00000000..85f9dd24 --- /dev/null +++ b/packages/react/utils/test/browser/index.test.tsx @@ -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[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("", () => { + it("Should reactively show an element", () => { + const toggle = signal(false)!; + const Paragraph = (p: any) =>

{p.children}

; + act(() => { + render( + Hiding}> + Showing + + ); + }); + expect(scratch.innerHTML).to.eq("

Hiding

"); + + act(() => { + toggle.value = true; + }); + expect(scratch.innerHTML).to.eq("

Showing

"); + }); + }); + + describe("", () => { + it("Should iterate over a list of signals", () => { + const list = signal>([])!; + const Paragraph = (p: any) =>

{p.children}

; + act(() => { + render( + No items}> + {item => {item}} + + ); + }); + expect(scratch.innerHTML).to.eq("

No items

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

foo

bar

"); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index b1e35434..2988026e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" ],