react-fine-grained-signals 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ import { t as ReadonlySignal } from "./base-Bx5ahfW2.js";
2
+ import { Key, ReactNode } from "react";
3
+ //#region src/utils.d.ts
4
+ /** A plain value or a value created by `signal`, `computed`, or `deepSignal`. */
5
+ type SignalInput<T> = T | ReadonlySignal<T>;
6
+ /** Renders its children only while `when` is truthy. */
7
+ interface ShowProps<T> {
8
+ when: SignalInput<T>;
9
+ fallback?: ReactNode;
10
+ children: ReactNode | ((value: NonNullable<T>) => ReactNode);
11
+ }
12
+ /**
13
+ * A small reactive conditional-rendering boundary.
14
+ *
15
+ * Unlike Solid's compiler-aware control flow, this is a normal React component:
16
+ * only this component rerenders when a signal read from `when` changes.
17
+ */
18
+ declare function Show<T>({ when, fallback, children }: ShowProps<T>): ReactNode;
19
+ /** A branch declaration consumed by the nearest `Switch`. */
20
+ interface MatchProps<T> {
21
+ when: SignalInput<T>;
22
+ children: ReactNode | ((value: NonNullable<T>) => ReactNode);
23
+ }
24
+ /**
25
+ * Declares a `Switch` branch. `Match` only has meaning as a child of `Switch`.
26
+ */
27
+ declare function Match<T>(_props: MatchProps<T>): null;
28
+ /** Chooses and renders the first truthy child `Match`. */
29
+ interface SwitchProps {
30
+ fallback?: ReactNode;
31
+ children?: ReactNode;
32
+ }
33
+ /**
34
+ * A small reactive multi-branch boundary inspired by Solid's `Switch`/`Match`.
35
+ */
36
+ declare function Switch({ fallback, children }: SwitchProps): ReactNode;
37
+ /** A collection supported by `For` when each item has its own identity. */
38
+ type ForCollection<T> = readonly T[] | ReadonlySet<T>;
39
+ /** Renders an array or set whose items have stable identities. */
40
+ interface ForProps<T> {
41
+ each: SignalInput<ForCollection<T> | null | undefined>;
42
+ fallback?: ReactNode;
43
+ /** Returns the stable React key for an item. It must be pure and data-derived. */
44
+ by: (item: T, index: number) => Key;
45
+ children: (item: T, index: number) => ReactNode;
46
+ }
47
+ /** Renders a map whose entries have stable identities. */
48
+ interface ForMapProps<K, V> {
49
+ each: SignalInput<ReadonlyMap<K, V> | null | undefined>;
50
+ fallback?: ReactNode;
51
+ /** Returns the stable React key for an entry. It must be pure and data-derived. */
52
+ by: (entry: readonly [K, V], index: number) => Key;
53
+ children: (entry: readonly [K, V], index: number) => ReactNode;
54
+ }
55
+ /**
56
+ * A React list boundary inspired by Solid's `For`.
57
+ *
58
+ * React still owns reconciliation. `by` supplies stable keys, so use `Index`
59
+ * instead when a list's identity is intentionally positional.
60
+ */
61
+ declare function For<T>(props: ForProps<T>): ReactNode;
62
+ declare function For<K, V>(props: ForMapProps<K, V>): ReactNode;
63
+ /** Renders an array whose row identity is intentionally its position. */
64
+ interface IndexProps<T> {
65
+ each: SignalInput<readonly T[] | null | undefined>;
66
+ fallback?: ReactNode;
67
+ /** Receives a render-time accessor for the current value at this position. */
68
+ children: (item: () => T, index: number) => ReactNode;
69
+ }
70
+ /**
71
+ * A position-keyed React list boundary inspired by Solid's `Index`.
72
+ *
73
+ * The accessor should be read during render. For identity-keyed lists, use
74
+ * `For` instead and provide `by`.
75
+ */
76
+ declare function Index<T>({ each, fallback, children }: IndexProps<T>): ReactNode;
77
+ //#endregion
78
+ export { For, ForCollection, ForMapProps, ForProps, Index, IndexProps, Match, MatchProps, Show, ShowProps, SignalInput, Switch, SwitchProps };
package/dist/utils.js ADDED
@@ -0,0 +1,91 @@
1
+ import { o as isSignal } from "./base-BZTnpU8U.js";
2
+ import { t as useManagedSignals } from "./use-signals-8i2B-94o.js";
3
+ import { Children, Fragment, isValidElement } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/utils.tsx
6
+ /**
7
+ * A small reactive conditional-rendering boundary.
8
+ *
9
+ * Unlike Solid's compiler-aware control flow, this is a normal React component:
10
+ * only this component rerenders when a signal read from `when` changes.
11
+ */
12
+ function Show({ when, fallback = null, children }) {
13
+ const store = useManagedSignals();
14
+ try {
15
+ const value = readSignalInput(when);
16
+ if (!value) return fallback;
17
+ return typeof children === "function" ? children(value) : children;
18
+ } finally {
19
+ store.f();
20
+ }
21
+ }
22
+ /**
23
+ * Declares a `Switch` branch. `Match` only has meaning as a child of `Switch`.
24
+ */
25
+ function Match(_props) {
26
+ return null;
27
+ }
28
+ /**
29
+ * A small reactive multi-branch boundary inspired by Solid's `Switch`/`Match`.
30
+ */
31
+ function Switch({ fallback = null, children }) {
32
+ const store = useManagedSignals();
33
+ try {
34
+ for (const match of collectMatches(children)) {
35
+ const value = readSignalInput(match.props.when);
36
+ if (!value) continue;
37
+ const branch = match.props.children;
38
+ return typeof branch === "function" ? branch(value) : branch;
39
+ }
40
+ return fallback;
41
+ } finally {
42
+ store.f();
43
+ }
44
+ }
45
+ function For({ each, fallback = null, by, children }) {
46
+ const store = useManagedSignals();
47
+ try {
48
+ const collection = readSignalInput(each);
49
+ if (collection === null || collection === void 0) return fallback;
50
+ const items = Array.isArray(collection) ? collection : Array.from(collection);
51
+ if (items.length === 0) return fallback;
52
+ return items.map((item, index) => /* @__PURE__ */ jsx(Fragment, { children: children(item, index) }, by(item, index)));
53
+ } finally {
54
+ store.f();
55
+ }
56
+ }
57
+ /**
58
+ * A position-keyed React list boundary inspired by Solid's `Index`.
59
+ *
60
+ * The accessor should be read during render. For identity-keyed lists, use
61
+ * `For` instead and provide `by`.
62
+ */
63
+ function Index({ each, fallback = null, children }) {
64
+ const store = useManagedSignals();
65
+ try {
66
+ const items = readSignalInput(each);
67
+ if (items === null || items === void 0 || items.length === 0) return fallback;
68
+ const readAt = (index) => (readSignalInput(each) ?? items)[index];
69
+ return items.map((_item, index) => /* @__PURE__ */ jsx(Fragment, { children: children(() => readAt(index), index) }, index));
70
+ } finally {
71
+ store.f();
72
+ }
73
+ }
74
+ function readSignalInput(value) {
75
+ return isSignal(value) ? value.value : value;
76
+ }
77
+ function collectMatches(children) {
78
+ const matches = [];
79
+ Children.forEach(children, (child) => {
80
+ if (!isValidElement(child)) return;
81
+ if (child.type === Fragment) {
82
+ const fragment = child;
83
+ matches.push(...collectMatches(fragment.props.children));
84
+ } else if (child.type === Match) matches.push(child);
85
+ });
86
+ return matches;
87
+ }
88
+ //#endregion
89
+ export { For, Index, Match, Show, Switch };
90
+
91
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../src/utils.tsx"],"sourcesContent":["import {\n Children,\n Fragment,\n isValidElement,\n type Key,\n type ReactElement,\n type ReactNode,\n} from \"react\";\nimport { isSignal, type ReadonlySignal } from \"./core/index.js\";\nimport { useManagedSignals } from \"./react/use-signals.js\";\n\n/*\n * These four components use the *managed* render scope rather than the\n * best-effort `useSignals()`. The library owns their entire function body, so\n * an exact boundary — open at the top, closed in a `finally` before the\n * component returns — costs nothing here, while `useSignals()`' scope can stay\n * open past the return (until the next `useSignals()` call, a layout effect, or\n * a microtask) and misattribute a *sibling's* render-time signal read to this\n * component's store, which then silently stops updating the sibling. Every\n * branch and early return must stay inside the `try`. See\n * docs/design/use-signals-boundary-design.md.\n */\n\n/** A plain value or a value created by `signal`, `computed`, or `deepSignal`. */\nexport type SignalInput<T> = T | ReadonlySignal<T>;\n\n/** Renders its children only while `when` is truthy. */\nexport interface ShowProps<T> {\n when: SignalInput<T>;\n fallback?: ReactNode;\n children: ReactNode | ((value: NonNullable<T>) => ReactNode);\n}\n\n/**\n * A small reactive conditional-rendering boundary.\n *\n * Unlike Solid's compiler-aware control flow, this is a normal React component:\n * only this component rerenders when a signal read from `when` changes.\n */\nexport function Show<T>({ when, fallback = null, children }: ShowProps<T>): ReactNode {\n const store = useManagedSignals();\n try {\n const value = readSignalInput(when);\n\n if (!value) return fallback;\n return typeof children === \"function\"\n ? children(value as NonNullable<T>)\n : children;\n } finally {\n store.f();\n }\n}\n\n/** A branch declaration consumed by the nearest `Switch`. */\nexport interface MatchProps<T> {\n when: SignalInput<T>;\n children: ReactNode | ((value: NonNullable<T>) => ReactNode);\n}\n\n/**\n * Declares a `Switch` branch. `Match` only has meaning as a child of `Switch`.\n */\nexport function Match<T>(_props: MatchProps<T>): null {\n return null;\n}\n\n/** Chooses and renders the first truthy child `Match`. */\nexport interface SwitchProps {\n fallback?: ReactNode;\n children?: ReactNode;\n}\n\n/**\n * A small reactive multi-branch boundary inspired by Solid's `Switch`/`Match`.\n */\nexport function Switch({ fallback = null, children }: SwitchProps): ReactNode {\n const store = useManagedSignals();\n try {\n for (const match of collectMatches(children)) {\n const value = readSignalInput(match.props.when);\n if (!value) continue;\n\n const branch = match.props.children;\n return typeof branch === \"function\"\n ? branch(value as NonNullable<unknown>)\n : branch;\n }\n\n return fallback;\n } finally {\n store.f();\n }\n}\n\n/** A collection supported by `For` when each item has its own identity. */\nexport type ForCollection<T> = readonly T[] | ReadonlySet<T>;\n\n/** Renders an array or set whose items have stable identities. */\nexport interface ForProps<T> {\n each: SignalInput<ForCollection<T> | null | undefined>;\n fallback?: ReactNode;\n /** Returns the stable React key for an item. It must be pure and data-derived. */\n by: (item: T, index: number) => Key;\n children: (item: T, index: number) => ReactNode;\n}\n\n/** Renders a map whose entries have stable identities. */\nexport interface ForMapProps<K, V> {\n each: SignalInput<ReadonlyMap<K, V> | null | undefined>;\n fallback?: ReactNode;\n /** Returns the stable React key for an entry. It must be pure and data-derived. */\n by: (entry: readonly [K, V], index: number) => Key;\n children: (entry: readonly [K, V], index: number) => ReactNode;\n}\n\n/**\n * A React list boundary inspired by Solid's `For`.\n *\n * React still owns reconciliation. `by` supplies stable keys, so use `Index`\n * instead when a list's identity is intentionally positional.\n */\nexport function For<T>(props: ForProps<T>): ReactNode;\nexport function For<K, V>(props: ForMapProps<K, V>): ReactNode;\nexport function For(\n { each, fallback = null, by, children }: ForProps<unknown> | ForMapProps<unknown, unknown>,\n): ReactNode {\n const store = useManagedSignals();\n try {\n const collection = readSignalInput(each);\n\n if (collection === null || collection === undefined) {\n return fallback;\n }\n\n const items = Array.isArray(collection) ? collection : Array.from(collection);\n if (items.length === 0) return fallback;\n\n return items.map((item, index) => (\n <Fragment key={by(item, index)}>{children(item, index)}</Fragment>\n ));\n } finally {\n store.f();\n }\n}\n\n/** Renders an array whose row identity is intentionally its position. */\nexport interface IndexProps<T> {\n each: SignalInput<readonly T[] | null | undefined>;\n fallback?: ReactNode;\n /** Receives a render-time accessor for the current value at this position. */\n children: (item: () => T, index: number) => ReactNode;\n}\n\n/**\n * A position-keyed React list boundary inspired by Solid's `Index`.\n *\n * The accessor should be read during render. For identity-keyed lists, use\n * `For` instead and provide `by`.\n */\nexport function Index<T>({ each, fallback = null, children }: IndexProps<T>): ReactNode {\n const store = useManagedSignals();\n try {\n const items = readSignalInput(each);\n\n if (items === null || items === undefined || items.length === 0) {\n return fallback;\n }\n\n // Reads back through `each` on every call rather than closing over the\n // render-time `items` snapshot, so an accessor a child stores and calls\n // later (from an event handler, say) still sees the current value instead\n // of a stale row. The snapshot is only the fallback for a collection that\n // has since become null/undefined. `as T` stands in for the old `!`: an\n // out-of-range position reads as `undefined`, exactly as before, and a\n // sparse hole is no longer disguised as a present value.\n const readAt = (index: number): T =>\n (readSignalInput(each) ?? items)[index] as T;\n\n return items.map((_item, index) => (\n <Fragment key={index}>{children(() => readAt(index), index)}</Fragment>\n ));\n } finally {\n store.f();\n }\n}\n\nfunction readSignalInput<T>(value: SignalInput<T>): T {\n return isSignal(value) ? (value.value as T) : value;\n}\n\nfunction collectMatches(children: ReactNode): Array<ReactElement<MatchProps<unknown>>> {\n const matches: Array<ReactElement<MatchProps<unknown>>> = [];\n\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) return;\n if (child.type === Fragment) {\n const fragment = child as ReactElement<{ children?: ReactNode }>;\n matches.push(...collectMatches(fragment.props.children));\n } else if (child.type === Match) {\n matches.push(child as ReactElement<MatchProps<unknown>>);\n }\n });\n\n return matches;\n}\n"],"mappings":";;;;;;;;;;;AAuCA,SAAgB,KAAQ,EAAE,MAAM,WAAW,MAAM,YAAqC;CACpF,MAAM,QAAQ,kBAAkB;CAChC,IAAI;EACF,MAAM,QAAQ,gBAAgB,IAAI;EAElC,IAAI,CAAC,OAAO,OAAO;EACnB,OAAO,OAAO,aAAa,aACvB,SAAS,KAAuB,IAChC;CACN,UAAU;EACR,MAAM,EAAE;CACV;AACF;;;;AAWA,SAAgB,MAAS,QAA6B;CACpD,OAAO;AACT;;;;AAWA,SAAgB,OAAO,EAAE,WAAW,MAAM,YAAoC;CAC5E,MAAM,QAAQ,kBAAkB;CAChC,IAAI;EACF,KAAK,MAAM,SAAS,eAAe,QAAQ,GAAG;GAC5C,MAAM,QAAQ,gBAAgB,MAAM,MAAM,IAAI;GAC9C,IAAI,CAAC,OAAO;GAEZ,MAAM,SAAS,MAAM,MAAM;GAC3B,OAAO,OAAO,WAAW,aACrB,OAAO,KAA6B,IACpC;EACN;EAEA,OAAO;CACT,UAAU;EACR,MAAM,EAAE;CACV;AACF;AA+BA,SAAgB,IACd,EAAE,MAAM,WAAW,MAAM,IAAI,YAClB;CACX,MAAM,QAAQ,kBAAkB;CAChC,IAAI;EACF,MAAM,aAAa,gBAAgB,IAAI;EAEvC,IAAI,eAAe,QAAQ,eAAe,KAAA,GACxC,OAAO;EAGT,MAAM,QAAQ,MAAM,QAAQ,UAAU,IAAI,aAAa,MAAM,KAAK,UAAU;EAC5E,IAAI,MAAM,WAAW,GAAG,OAAO;EAE/B,OAAO,MAAM,KAAK,MAAM,UACtB,oBAAC,UAAD,EAAA,UAAiC,SAAS,MAAM,KAAK,EAAY,GAAlD,GAAG,MAAM,KAAK,CAAoC,CAClE;CACH,UAAU;EACR,MAAM,EAAE;CACV;AACF;;;;;;;AAgBA,SAAgB,MAAS,EAAE,MAAM,WAAW,MAAM,YAAsC;CACtF,MAAM,QAAQ,kBAAkB;CAChC,IAAI;EACF,MAAM,QAAQ,gBAAgB,IAAI;EAElC,IAAI,UAAU,QAAQ,UAAU,KAAA,KAAa,MAAM,WAAW,GAC5D,OAAO;EAUT,MAAM,UAAU,WACb,gBAAgB,IAAI,KAAK,MAAA,CAAO;EAEnC,OAAO,MAAM,KAAK,OAAO,UACvB,oBAAC,UAAD,EAAA,UAAuB,eAAe,OAAO,KAAK,GAAG,KAAK,EAAY,GAAvD,KAAuD,CACvE;CACH,UAAU;EACR,MAAM,EAAE;CACV;AACF;AAEA,SAAS,gBAAmB,OAA0B;CACpD,OAAO,SAAS,KAAK,IAAK,MAAM,QAAc;AAChD;AAEA,SAAS,eAAe,UAA+D;CACrF,MAAM,UAAoD,CAAC;CAE3D,SAAS,QAAQ,WAAW,UAAU;EACpC,IAAI,CAAC,eAAe,KAAK,GAAG;EAC5B,IAAI,MAAM,SAAS,UAAU;GAC3B,MAAM,WAAW;GACjB,QAAQ,KAAK,GAAG,eAAe,SAAS,MAAM,QAAQ,CAAC;EACzD,OAAO,IAAI,MAAM,SAAS,OACxB,QAAQ,KAAK,KAA0C;CAE3D,CAAC;CAED,OAAO;AACT"}
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "react-fine-grained-signals",
3
+ "version": "0.1.0",
4
+ "description": "Fine-grained signal rendering for React 19, built on alien-signals.",
5
+ "license": "MIT",
6
+ "author": "akazawa",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/sakzw/react-fine-grained-signals.git"
10
+ },
11
+ "homepage": "https://github.com/sakzw/react-fine-grained-signals#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/sakzw/react-fine-grained-signals/issues"
14
+ },
15
+ "keywords": [
16
+ "react",
17
+ "signals",
18
+ "signal",
19
+ "fine-grained",
20
+ "reactivity",
21
+ "jsx",
22
+ "jsx-runtime",
23
+ "alien-signals"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "engines": {
28
+ "node": ">=22.0.0"
29
+ },
30
+ "devEngines": {
31
+ "runtime": {
32
+ "name": "node",
33
+ "version": "24.20.0",
34
+ "onFail": "download"
35
+ }
36
+ },
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "import": "./dist/index.js"
41
+ },
42
+ "./jsx-runtime": {
43
+ "types": "./dist/jsx-runtime.d.ts",
44
+ "import": "./dist/jsx-runtime.js"
45
+ },
46
+ "./jsx-dev-runtime": {
47
+ "types": "./dist/jsx-dev-runtime.d.ts",
48
+ "import": "./dist/jsx-dev-runtime.js"
49
+ },
50
+ "./runtime": {
51
+ "types": "./dist/runtime.d.ts",
52
+ "import": "./dist/runtime.js"
53
+ },
54
+ "./utils": {
55
+ "types": "./dist/utils.d.ts",
56
+ "import": "./dist/utils.js"
57
+ }
58
+ },
59
+ "files": [
60
+ "dist"
61
+ ],
62
+ "peerDependencies": {
63
+ "alien-signals": "^3.2.1",
64
+ "react": ">=19.0.0"
65
+ },
66
+ "devDependencies": {
67
+ "@playwright/test": "^1.62.1",
68
+ "@preact/signals-core": "1.14.4",
69
+ "@testing-library/react": "^16.3.3",
70
+ "@types/node": "^24.13.3",
71
+ "@types/react": "^19.2.18",
72
+ "@types/react-dom": "^19.2.5",
73
+ "@vitest/coverage-v8": "4.1.11",
74
+ "alien-signals": "^3.2.1",
75
+ "babel-plugin-react-compiler": "^1.0.0",
76
+ "jsdom": "^30.0.1",
77
+ "oxlint": "^1.80.0",
78
+ "react": "^19.2.8",
79
+ "react-dom": "^19.2.8",
80
+ "tsdown": "0.22.14",
81
+ "typescript": "^7.0.2",
82
+ "vite": "^8.2.2",
83
+ "vitest": "^4.1.11",
84
+ "node": "runtime:24.20.0"
85
+ },
86
+ "scripts": {
87
+ "bench": "pnpm build:runtime && node --expose-gc benchmarks/core.mjs",
88
+ "bench:deep": "pnpm build:runtime && node --expose-gc benchmarks/deep-signal.mjs",
89
+ "bench:react": "pnpm build:runtime && node --expose-gc benchmarks/react-render.mjs",
90
+ "bench:transform": "pnpm build:transform && pnpm --filter unplugin-react-fine-grained-signals bench",
91
+ "build": "pnpm build:runtime && pnpm build:transform",
92
+ "build:runtime": "tsdown && node scripts/strip-dts-sourcemap-comments.mjs",
93
+ "build:transform": "pnpm --filter unplugin-react-fine-grained-signals build",
94
+ "dev:browser": "pnpm build:transform && node examples/browser/server.mjs",
95
+ "build:browser": "pnpm build:transform && vite build --config examples/browser/vite.config.ts",
96
+ "preview:browser": "vite preview --config examples/browser/vite.config.ts --host 127.0.0.1 --port 4174 --strictPort",
97
+ "typecheck": "pnpm typecheck:runtime && pnpm typecheck:transform",
98
+ "typecheck:runtime": "tsc --noEmit",
99
+ "typecheck:transform": "pnpm --filter unplugin-react-fine-grained-signals typecheck",
100
+ "lint": "oxlint",
101
+ "lint:fix": "oxlint --fix",
102
+ "test": "pnpm test:runtime && pnpm test:transform",
103
+ "test:runtime": "vitest run",
104
+ "test:transform": "pnpm --filter unplugin-react-fine-grained-signals test",
105
+ "test:coverage": "pnpm test:coverage:runtime && pnpm test:coverage:transform",
106
+ "test:coverage:runtime": "vitest run --coverage",
107
+ "test:coverage:transform": "pnpm --filter unplugin-react-fine-grained-signals test:coverage",
108
+ "test:consumer": "pnpm build && node tests/consumer-smoke.mjs",
109
+ "prepare:e2e": "pnpm --dir examples/react-router install --frozen-lockfile",
110
+ "test:browser": "playwright test",
111
+ "size": "pnpm build:runtime && node scripts/check-size.mjs",
112
+ "size:update": "pnpm build:runtime && node scripts/check-size.mjs --update"
113
+ }
114
+ }