@octanejs/zag 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/UPSTREAM.md +76 -0
- package/package.json +71 -0
- package/src/bindable.ts +86 -0
- package/src/index.ts +5 -0
- package/src/internal.ts +32 -0
- package/src/machine.ts +352 -0
- package/src/normalize-props.ts +68 -0
- package/src/portal.ts +49 -0
- package/src/refs.ts +15 -0
- package/src/track.ts +33 -0
- package/src/use-layout-effect.ts +4 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @octanejs/zag
|
|
2
|
+
|
|
3
|
+
[Zag](https://zagjs.com/) state-machine bindings for the
|
|
4
|
+
[Octane](https://github.com/octanejs/octane) UI framework.
|
|
5
|
+
|
|
6
|
+
This package ports `@zag-js/react@1.42.0` onto Octane while reusing the
|
|
7
|
+
framework-agnostic `@zag-js/core`, `@zag-js/store`, `@zag-js/types`, and
|
|
8
|
+
`@zag-js/utils` packages unchanged. It is the runtime substrate for Octane
|
|
9
|
+
ports of component libraries built on Zag, including Ark UI and Chakra UI.
|
|
10
|
+
|
|
11
|
+
Change the framework adapter import while keeping machine definitions and
|
|
12
|
+
connect functions on their existing `@zag-js/*` packages:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// before
|
|
16
|
+
import { normalizeProps, useMachine } from '@zag-js/react';
|
|
17
|
+
|
|
18
|
+
// after
|
|
19
|
+
import { normalizeProps, useMachine } from '@octanejs/zag';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { normalizeProps, useMachine } from '@octanejs/zag';
|
|
24
|
+
import * as toggle from '@zag-js/toggle';
|
|
25
|
+
|
|
26
|
+
export function Toggle() @{
|
|
27
|
+
const service = useMachine(toggle.machine, { id: 'example' });
|
|
28
|
+
const api = toggle.connect(service, normalizeProps);
|
|
29
|
+
|
|
30
|
+
<button {...api.getRootProps()}>{api.pressed ? 'On' : 'Off'}</button>
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
The public surface matches `@zag-js/react@1.42.0`:
|
|
37
|
+
|
|
38
|
+
- `useMachine` runs a Zag machine with Octane hooks.
|
|
39
|
+
- `normalizeProps` types Zag-generated host props for Octane JSX.
|
|
40
|
+
- `Portal` renders into `document.body` or a supplied container and supports
|
|
41
|
+
disabled and server-rendered in-place output.
|
|
42
|
+
- `mergeProps` is re-exported from `@zag-js/core`.
|
|
43
|
+
- `useSyncExternalStore` is re-exported from Octane.
|
|
44
|
+
|
|
45
|
+
The adapter forwards Octane's compiler-assigned hook slots through every
|
|
46
|
+
composed hook and through Zag's indirect `bindable` and `track` callbacks.
|
|
47
|
+
Distinct machines and bindable context values therefore remain independent.
|
|
48
|
+
|
|
49
|
+
## Verification
|
|
50
|
+
|
|
51
|
+
Behavioral tests cover transitions, bindable context, independent hook call
|
|
52
|
+
sites, multi-child portals, disabled portals, and server rendering. A
|
|
53
|
+
differential test runs the same machine through this package and
|
|
54
|
+
`@zag-js/react@1.42.0` and compares the state/context trace. Published source
|
|
55
|
+
and dependency checks ensure React is development-only.
|
|
56
|
+
|
|
57
|
+
Current scope and verification status are tracked in the generated
|
|
58
|
+
[bindings status table](../../docs/bindings-status.md), sourced from this
|
|
59
|
+
package's [`status.json`](./status.json).
|
package/UPSTREAM.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @zag-js/react upstream contract
|
|
2
|
+
|
|
3
|
+
## Pin and source boundary
|
|
4
|
+
|
|
5
|
+
| Field | Value |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Package | `@zag-js/react` |
|
|
8
|
+
| Version | `1.42.0` |
|
|
9
|
+
| Canonical tag commit | `df65e4c87c75a1c84eb6eb08a8e30dac0e1bb77f` |
|
|
10
|
+
| Supported upstream range | exactly `1.42.0` |
|
|
11
|
+
| React oracle | `19.2.7` with `@types/react@19.2.7` and `@types/react-dom@19.2.3` (`catalog:zag-react-oracle`) |
|
|
12
|
+
| Testing Library oracle | `@testing-library/react@16.3.2` (`catalog:zag-react-oracle`; satisfies upstream `^16.3.2`) |
|
|
13
|
+
| jsdom oracle | `29.0.1` (`catalog:zag-react-oracle`; satisfies upstream `^29.0.1`) |
|
|
14
|
+
| Vendored tree integrity | `sha256:037f857d00462559b9790acc2214c818678eb9d61970d6c394dc26dcfcd1516d` (`packages/zag/upstream/SHA256SUMS`) |
|
|
15
|
+
| License | MIT, © Chakra UI |
|
|
16
|
+
|
|
17
|
+
Repository: `https://github.com/chakra-ui/zag.git`. The npm artifact publishes
|
|
18
|
+
compiled `dist/` output. The framework adapter source and its Vitest suite at
|
|
19
|
+
this pin live under `packages/frameworks/react` in the canonical repository and
|
|
20
|
+
are vendored byte-exact at `packages/zag/upstream/` (MIT). Framework-agnostic
|
|
21
|
+
`@zag-js/core`, `@zag-js/store`, `@zag-js/types`, and `@zag-js/utils` at the same
|
|
22
|
+
version are reused unchanged and are not reimplemented here.
|
|
23
|
+
|
|
24
|
+
## Runtime export crosswalk
|
|
25
|
+
|
|
26
|
+
| Upstream export | Octane disposition | Evidence |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `useMachine` | Ported to Octane hooks in `src/machine.ts` | `tests/upstream/machine.test.ts`, `tests/upstream/nested-states.test.ts`, `tests/differential/machine.test.ts` |
|
|
29
|
+
| `normalizeProps` | Ported in `src/normalize-props.ts` with documented text-host event rewrite | Framework-contract: `tests/conformance/upstream-surface.test.ts`. Not an upstream-suite identity; see Intentional divergences. |
|
|
30
|
+
| `Portal` | Ported in `src/portal.ts` | Differential: `tests/differential/portal.test.ts`. Framework-contract / SSR pins: `tests/conformance/machine.test.ts`, `tests/ssr/server.test.ts` (not counted as React-parity identities). |
|
|
31
|
+
| `mergeProps` | Re-exported from `@zag-js/core` | Structural export inventory in `tests/conformance/upstream-surface.test.ts` |
|
|
32
|
+
| `useSyncExternalStore` | Re-exported from Octane | Structural export inventory in `tests/conformance/upstream-surface.test.ts` |
|
|
33
|
+
|
|
34
|
+
## Intentional divergences
|
|
35
|
+
|
|
36
|
+
- **Text-host `onChange` → `onInput`.** Upstream `normalizeProps` is an identity
|
|
37
|
+
normalizer. Octane rewrites React-style text-entry `onChange` to native
|
|
38
|
+
`onInput` for `normalizeProps.textarea` and `normalizeProps.input` when the
|
|
39
|
+
type is a text-entry host (`text`/`search`/`url`/`tel`/`password`/`email`/
|
|
40
|
+
`number`, plus missing/invalid types that HTML defaults to text).
|
|
41
|
+
`<select>` and non-text inputs (`checkbox`/`radio`/`file`/`date`/`range`/
|
|
42
|
+
…) keep native `onChange`. Upstream has no cases for this export; the rewrite
|
|
43
|
+
is pinned by `tests/conformance/upstream-surface.test.ts` and recorded in
|
|
44
|
+
`status.json`.
|
|
45
|
+
- **Portal ref / children shape.** Container refs use Octane's structural
|
|
46
|
+
`{ current: HTMLElement | null }` rather than `React.RefObject`. Compiled
|
|
47
|
+
Octane children blocks portal as one unit so component scope is preserved;
|
|
48
|
+
ordinary value children keep upstream's per-child portal behavior. Mount /
|
|
49
|
+
disabled / root-node behavior is compared against `@zag-js/react@1.42.0` in
|
|
50
|
+
`tests/differential/portal.test.ts`.
|
|
51
|
+
|
|
52
|
+
## Test-suite disposition
|
|
53
|
+
|
|
54
|
+
Upstream `@zag-js/react` ships a Vitest runtime suite under
|
|
55
|
+
`packages/frameworks/react/tests` at this pin (`machine.test.ts`,
|
|
56
|
+
`nested-states.test.ts`, `strict-mode.test.tsx`, plus `render.ts`,
|
|
57
|
+
`vite.config.ts`, and `vitest.setup.ts`). That suite is recorded as present and
|
|
58
|
+
locked under `packages/zag/upstream/`.
|
|
59
|
+
|
|
60
|
+
| Upstream artifact | Disposition | Octane location / notes |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `tests/machine.test.ts` | Ported one-for-one | `tests/upstream/machine.test.ts` |
|
|
63
|
+
| `tests/nested-states.test.ts` | Ported one-for-one | `tests/upstream/nested-states.test.ts` |
|
|
64
|
+
| `tests/strict-mode.test.tsx` | Out of scope for adapted Octane lane | React StrictMode double-invoke is an intentional Octane non-feature; still executed in the pristine React lane. Exact pristine case IDs and rationale are locked in `audit/runtime-case-dispositions.json` and checked by the pristine→adapted inventory crosswalk. |
|
|
65
|
+
| `tests/render.ts` | Ported helper | `tests/upstream/render.ts` |
|
|
66
|
+
|
|
67
|
+
Parity evidence:
|
|
68
|
+
|
|
69
|
+
- pristine full suite: `zag-pristine` Vitest project / `tests/upstream-original.test.ts`
|
|
70
|
+
- adapted full suite: `zag` Vitest project / `tests/upstream/**/*.test.ts`
|
|
71
|
+
- differential (supplementary): `zag-differential` / `tests/differential/*.test.ts`
|
|
72
|
+
- pristine types: `packages/zag/audit/type-probes` via `tsc`
|
|
73
|
+
- adapted types: `packages/zag/typetests` via `tsrx-tsc`
|
|
74
|
+
|
|
75
|
+
Conformance and SSR tests remain ordinary `zag` / `zag-ssr` package coverage and
|
|
76
|
+
are not counted as adapted upstream-suite identities.
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/zag",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.22.2"
|
|
8
|
+
},
|
|
9
|
+
"octane": {
|
|
10
|
+
"hookSlots": {
|
|
11
|
+
"manual": [
|
|
12
|
+
"src"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"description": "Zag state-machine bindings for Octane, backed by the framework-agnostic @zag-js core packages.",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Dominic Gannaway",
|
|
19
|
+
"email": "dg@domgan.com"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
27
|
+
"directory": "packages/zag"
|
|
28
|
+
},
|
|
29
|
+
"main": "src/index.ts",
|
|
30
|
+
"module": "src/index.ts",
|
|
31
|
+
"types": "src/index.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"README.md",
|
|
35
|
+
"UPSTREAM.md"
|
|
36
|
+
],
|
|
37
|
+
"exports": {
|
|
38
|
+
".": "./src/index.ts"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@zag-js/core": "1.42.0",
|
|
42
|
+
"@zag-js/store": "1.42.0",
|
|
43
|
+
"@zag-js/types": "1.42.0",
|
|
44
|
+
"@zag-js/utils": "1.42.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"octane": "0.1.35"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@tsrx/react": "^0.2.56",
|
|
51
|
+
"@types/react": "19.2.7",
|
|
52
|
+
"@types/react-dom": "19.2.3",
|
|
53
|
+
"@zag-js/react": "1.42.0",
|
|
54
|
+
"esbuild": "^0.28.1",
|
|
55
|
+
"react": "19.2.7",
|
|
56
|
+
"react-dom": "19.2.7",
|
|
57
|
+
"vitest": "^4.1.10",
|
|
58
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
59
|
+
"@testing-library/react": "16.3.2",
|
|
60
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
61
|
+
"jsdom": "29.0.1",
|
|
62
|
+
"octane": "0.1.35",
|
|
63
|
+
"@octanejs/testing-library": "0.1.32"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"test": "vitest run --root ../.. --project zag --project zag-differential --project zag-ssr --project zag-pristine",
|
|
67
|
+
"test:upstream": "node scripts/run-pristine-upstream.mjs",
|
|
68
|
+
"typecheck": "tsrx-tsc --noEmit -p tsconfig.json && tsrx-tsc --noEmit -p typetests/tsconfig.json",
|
|
69
|
+
"upstream:verify": "node scripts/verify-upstream.mjs"
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/bindable.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Bindable, BindableParams } from '@zag-js/core';
|
|
2
|
+
import { isFunction } from '@zag-js/utils';
|
|
3
|
+
import { flushSync, useEffect, useRef, useState } from 'octane';
|
|
4
|
+
|
|
5
|
+
import { subSlot } from './internal';
|
|
6
|
+
import { useSafeLayoutEffect } from './use-layout-effect';
|
|
7
|
+
|
|
8
|
+
function useBindableValue<T>(
|
|
9
|
+
props: () => BindableParams<T>,
|
|
10
|
+
slot: symbol | undefined,
|
|
11
|
+
): Bindable<T> {
|
|
12
|
+
const initial = props().value ?? props().defaultValue;
|
|
13
|
+
const eq = props().isEqual ?? Object.is;
|
|
14
|
+
|
|
15
|
+
const [initialValue] = useState(initial, subSlot(slot, 'initial'));
|
|
16
|
+
const [value, setValue] = useState(initialValue, subSlot(slot, 'value'));
|
|
17
|
+
const controlled = props().value !== undefined;
|
|
18
|
+
|
|
19
|
+
const valueRef = useRef(value, subSlot(slot, 'value-ref'));
|
|
20
|
+
valueRef.current = controlled ? props().value : value;
|
|
21
|
+
|
|
22
|
+
const prevValue = useRef(valueRef.current, subSlot(slot, 'previous-value'));
|
|
23
|
+
useSafeLayoutEffect(
|
|
24
|
+
() => {
|
|
25
|
+
prevValue.current = valueRef.current;
|
|
26
|
+
},
|
|
27
|
+
[value, props().value],
|
|
28
|
+
subSlot(slot, 'sync-previous'),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const setFn = (nextValue: T | ((prev: T) => T)) => {
|
|
32
|
+
const previous = prevValue.current;
|
|
33
|
+
const next = isFunction(nextValue) ? nextValue(previous as T) : nextValue;
|
|
34
|
+
|
|
35
|
+
if (props().debug) {
|
|
36
|
+
console.log(`[bindable > ${props().debug}] setValue`, { next, prev: previous });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Controlled values stay pinned to props().value (re-synced each render).
|
|
40
|
+
// Uncontrolled updates advance valueRef immediately so mid-transition
|
|
41
|
+
// action chains read the value set() already wrote.
|
|
42
|
+
if (!controlled) {
|
|
43
|
+
valueRef.current = next;
|
|
44
|
+
prevValue.current = next;
|
|
45
|
+
setValue(next);
|
|
46
|
+
}
|
|
47
|
+
if (!eq(next, previous)) props().onChange?.(next, previous);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
initial: initialValue,
|
|
52
|
+
ref: valueRef,
|
|
53
|
+
get() {
|
|
54
|
+
return (controlled ? props().value : valueRef.current) as T;
|
|
55
|
+
},
|
|
56
|
+
set(nextValue: T | ((prev: T) => T)) {
|
|
57
|
+
const execute = props().sync ? flushSync : (run: () => void) => run();
|
|
58
|
+
execute(() => setFn(nextValue));
|
|
59
|
+
},
|
|
60
|
+
invoke(nextValue: T, previousValue: T) {
|
|
61
|
+
props().onChange?.(nextValue, previousValue);
|
|
62
|
+
},
|
|
63
|
+
hash(nextValue: T) {
|
|
64
|
+
return props().hash?.(nextValue) ?? String(nextValue);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cleanup(fn: VoidFunction, slot?: symbol) {
|
|
70
|
+
useEffect(() => fn, [], subSlot(slot, 'cleanup'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function bindableRef<T>(defaultValue: T, slot?: symbol) {
|
|
74
|
+
const value = useRef(defaultValue, subSlot(slot, 'ref'));
|
|
75
|
+
return {
|
|
76
|
+
get: () => value.current,
|
|
77
|
+
set: (next: T) => {
|
|
78
|
+
value.current = next;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const useBindable = Object.assign(
|
|
84
|
+
<T>(props: () => BindableParams<T>, slot?: symbol) => useBindableValue(props, slot),
|
|
85
|
+
{ cleanup, ref: bindableRef },
|
|
86
|
+
);
|
package/src/index.ts
ADDED
package/src/internal.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const subSlotCache = new Map<symbol, Map<string, symbol>>();
|
|
2
|
+
const bareSlotCache = new Map<string, symbol>();
|
|
3
|
+
|
|
4
|
+
export function subSlot(slot: symbol | undefined, tag: string): symbol {
|
|
5
|
+
if (slot === undefined) {
|
|
6
|
+
let bare = bareSlotCache.get(tag);
|
|
7
|
+
if (bare === undefined) {
|
|
8
|
+
bare = Symbol.for(`@octanejs/zag:${tag}`);
|
|
9
|
+
bareSlotCache.set(tag, bare);
|
|
10
|
+
}
|
|
11
|
+
return bare;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let byTag = subSlotCache.get(slot);
|
|
15
|
+
if (byTag === undefined) {
|
|
16
|
+
byTag = new Map();
|
|
17
|
+
subSlotCache.set(slot, byTag);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let child = byTag.get(tag);
|
|
21
|
+
if (child === undefined) {
|
|
22
|
+
child = Symbol.for(`${slot.description ?? ''}:${tag}`);
|
|
23
|
+
byTag.set(tag, child);
|
|
24
|
+
}
|
|
25
|
+
return child;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function splitSlot(args: unknown[]): [unknown[], symbol | undefined] {
|
|
29
|
+
const tail = args[args.length - 1];
|
|
30
|
+
const slot = typeof tail === 'symbol' ? tail : undefined;
|
|
31
|
+
return [slot === undefined ? args : args.slice(0, -1), slot];
|
|
32
|
+
}
|
package/src/machine.ts
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionsOrFn,
|
|
3
|
+
Bindable,
|
|
4
|
+
BindableContext,
|
|
5
|
+
BindableRefs,
|
|
6
|
+
ChooseFn,
|
|
7
|
+
ComputedFn,
|
|
8
|
+
EffectsOrFn,
|
|
9
|
+
GuardFn,
|
|
10
|
+
Machine,
|
|
11
|
+
MachineSchema,
|
|
12
|
+
Params,
|
|
13
|
+
Service,
|
|
14
|
+
} from '@zag-js/core';
|
|
15
|
+
import {
|
|
16
|
+
createScope,
|
|
17
|
+
findTransition,
|
|
18
|
+
getExitEnterStates,
|
|
19
|
+
hasTag,
|
|
20
|
+
INIT_STATE,
|
|
21
|
+
MachineStatus,
|
|
22
|
+
matchesState,
|
|
23
|
+
resolveStateValue,
|
|
24
|
+
} from '@zag-js/core';
|
|
25
|
+
import { callAll, compact, ensure, isFunction, isString, toArray, warn } from '@zag-js/utils';
|
|
26
|
+
import { flushSync, useMemo, useRef } from 'octane';
|
|
27
|
+
|
|
28
|
+
import { useBindable } from './bindable';
|
|
29
|
+
import { splitSlot, subSlot } from './internal';
|
|
30
|
+
import { useRefs } from './refs';
|
|
31
|
+
import { useTrack } from './track';
|
|
32
|
+
import { useSafeLayoutEffect } from './use-layout-effect';
|
|
33
|
+
|
|
34
|
+
export function useMachine<T extends MachineSchema>(
|
|
35
|
+
machine: Machine<T>,
|
|
36
|
+
userProps?: Partial<T['props']>,
|
|
37
|
+
): Service<T>;
|
|
38
|
+
export function useMachine<T extends MachineSchema>(...rawArgs: unknown[]): Service<T> {
|
|
39
|
+
const [args, slot] = splitSlot(rawArgs);
|
|
40
|
+
const [machine, suppliedProps = {}] = args as [Machine<T>, Partial<T['props']>?];
|
|
41
|
+
const userProps = suppliedProps ?? {};
|
|
42
|
+
|
|
43
|
+
const scope = useMemo(
|
|
44
|
+
() => {
|
|
45
|
+
const { id, ids, getRootNode } = userProps as any;
|
|
46
|
+
return createScope({ id, ids, getRootNode });
|
|
47
|
+
},
|
|
48
|
+
[userProps],
|
|
49
|
+
subSlot(slot, 'scope'),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const debug = (...args: any[]) => {
|
|
53
|
+
if (machine.debug) console.log(...args);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const props: any = machine.props?.({ props: compact(userProps), scope }) ?? userProps;
|
|
57
|
+
const prop = useProp(props, subSlot(slot, 'prop'));
|
|
58
|
+
|
|
59
|
+
let bindableIndex = 0;
|
|
60
|
+
let cleanupIndex = 0;
|
|
61
|
+
let bindableRefIndex = 0;
|
|
62
|
+
const bindable = Object.assign(
|
|
63
|
+
<TValue>(params: () => any) =>
|
|
64
|
+
useBindable<TValue>(params, subSlot(slot, `context-bindable:${bindableIndex++}`)),
|
|
65
|
+
{
|
|
66
|
+
cleanup: (fn: VoidFunction) =>
|
|
67
|
+
useBindable.cleanup(fn, subSlot(slot, `context-cleanup:${cleanupIndex++}`)),
|
|
68
|
+
ref: <TValue>(defaultValue: TValue) =>
|
|
69
|
+
useBindable.ref(defaultValue, subSlot(slot, `context-ref:${bindableRefIndex++}`)),
|
|
70
|
+
},
|
|
71
|
+
) as typeof useBindable;
|
|
72
|
+
|
|
73
|
+
const context = machine.context?.({
|
|
74
|
+
prop,
|
|
75
|
+
bindable,
|
|
76
|
+
scope,
|
|
77
|
+
flush,
|
|
78
|
+
getContext() {
|
|
79
|
+
return ctx as any;
|
|
80
|
+
},
|
|
81
|
+
getComputed() {
|
|
82
|
+
return computed as any;
|
|
83
|
+
},
|
|
84
|
+
getRefs() {
|
|
85
|
+
return refs as any;
|
|
86
|
+
},
|
|
87
|
+
getEvent() {
|
|
88
|
+
return getEvent();
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const contextRef = useLiveRef<any>(context, subSlot(slot, 'context'));
|
|
93
|
+
const ctx: BindableContext<T> = {
|
|
94
|
+
get(key) {
|
|
95
|
+
return contextRef.current?.[key].ref.current;
|
|
96
|
+
},
|
|
97
|
+
set(key, value) {
|
|
98
|
+
contextRef.current?.[key].set(value);
|
|
99
|
+
},
|
|
100
|
+
initial(key) {
|
|
101
|
+
return contextRef.current?.[key].initial;
|
|
102
|
+
},
|
|
103
|
+
hash(key) {
|
|
104
|
+
const current = contextRef.current?.[key].ref.current;
|
|
105
|
+
return contextRef.current?.[key].hash(current);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const effects = useRef(new Map<string, VoidFunction>(), subSlot(slot, 'effects'));
|
|
110
|
+
const transitionRef = useRef<any>(null, subSlot(slot, 'transition'));
|
|
111
|
+
const previousEventRef = useRef<any>(null, subSlot(slot, 'previous-event'));
|
|
112
|
+
const eventRef = useRef<any>({ type: '' }, subSlot(slot, 'event'));
|
|
113
|
+
|
|
114
|
+
const getEvent = () => ({
|
|
115
|
+
...eventRef.current,
|
|
116
|
+
current() {
|
|
117
|
+
return eventRef.current;
|
|
118
|
+
},
|
|
119
|
+
previous() {
|
|
120
|
+
return previousEventRef.current;
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const getState = () => ({
|
|
125
|
+
...state,
|
|
126
|
+
matches(...values: T['state'][]) {
|
|
127
|
+
return values.some((value) => matchesState(state.ref.current as string, value as string));
|
|
128
|
+
},
|
|
129
|
+
hasTag(tag: T['tag']) {
|
|
130
|
+
return hasTag(machine, state.ref.current, tag);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const refs: BindableRefs<T> = useRefs(
|
|
135
|
+
machine.refs?.({ prop, context: ctx }) ?? {},
|
|
136
|
+
subSlot(slot, 'refs'),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
let trackIndex = 0;
|
|
140
|
+
const track = (deps: any[], trackedEffect: VoidFunction) =>
|
|
141
|
+
useTrack(deps, trackedEffect, subSlot(slot, `track:${trackIndex++}`));
|
|
142
|
+
|
|
143
|
+
const getParams = (): Params<T> => ({
|
|
144
|
+
state: getState(),
|
|
145
|
+
context: ctx,
|
|
146
|
+
event: getEvent(),
|
|
147
|
+
prop,
|
|
148
|
+
send,
|
|
149
|
+
action,
|
|
150
|
+
guard,
|
|
151
|
+
track,
|
|
152
|
+
refs,
|
|
153
|
+
computed,
|
|
154
|
+
flush,
|
|
155
|
+
scope,
|
|
156
|
+
choose,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const action = (keys: ActionsOrFn<T> | undefined) => {
|
|
160
|
+
const strings = isFunction(keys) ? keys(getParams()) : keys;
|
|
161
|
+
if (!strings) return;
|
|
162
|
+
const functions = strings.map((key) => {
|
|
163
|
+
const fn = machine.implementations?.actions?.[key];
|
|
164
|
+
if (!fn) warn(`[zag-js] No implementation found for action "${JSON.stringify(key)}"`);
|
|
165
|
+
return fn;
|
|
166
|
+
});
|
|
167
|
+
for (const fn of functions) fn?.(getParams());
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const guard = (key: T['guard'] | GuardFn<T>) => {
|
|
171
|
+
if (isFunction(key)) return key(getParams());
|
|
172
|
+
const fn = machine.implementations?.guards?.[key];
|
|
173
|
+
if (!fn) warn(`[zag-js] No implementation found for guard "${JSON.stringify(key)}"`);
|
|
174
|
+
return fn?.(getParams());
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const effect = (keys: EffectsOrFn<T> | undefined) => {
|
|
178
|
+
const strings = isFunction(keys) ? keys(getParams()) : keys;
|
|
179
|
+
if (!strings) return;
|
|
180
|
+
const functions = strings.map((key) => {
|
|
181
|
+
const fn = machine.implementations?.effects?.[key];
|
|
182
|
+
if (!fn) warn(`[zag-js] No implementation found for effect "${JSON.stringify(key)}"`);
|
|
183
|
+
return fn;
|
|
184
|
+
});
|
|
185
|
+
const cleanups: VoidFunction[] = [];
|
|
186
|
+
for (const fn of functions) {
|
|
187
|
+
const cleanup = fn?.(getParams());
|
|
188
|
+
if (cleanup) cleanups.push(cleanup);
|
|
189
|
+
}
|
|
190
|
+
return () => cleanups.forEach((cleanup) => cleanup?.());
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const choose: ChooseFn<T> = (transitions) =>
|
|
194
|
+
toArray(transitions).find((transition) => {
|
|
195
|
+
let result = !transition.guard;
|
|
196
|
+
if (isString(transition.guard)) result = !!guard(transition.guard);
|
|
197
|
+
else if (isFunction(transition.guard)) result = transition.guard(getParams());
|
|
198
|
+
return result;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const computed: ComputedFn<T> = (key) => {
|
|
202
|
+
ensure(machine.computed, () => '[zag-js] No computed object found on machine');
|
|
203
|
+
const fn = machine.computed[key];
|
|
204
|
+
return fn({
|
|
205
|
+
context: ctx as any,
|
|
206
|
+
event: getEvent(),
|
|
207
|
+
prop,
|
|
208
|
+
refs,
|
|
209
|
+
scope,
|
|
210
|
+
computed: computed as any,
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const state = useBindable(
|
|
215
|
+
() => ({
|
|
216
|
+
defaultValue: resolveStateValue(machine, machine.initialState({ prop })),
|
|
217
|
+
onChange(nextState, previousState) {
|
|
218
|
+
const { exiting, entering } = getExitEnterStates(
|
|
219
|
+
machine,
|
|
220
|
+
previousState,
|
|
221
|
+
nextState,
|
|
222
|
+
transitionRef.current?.reenter,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
exiting.forEach((item) => {
|
|
226
|
+
effects.current.get(item.path)?.();
|
|
227
|
+
effects.current.delete(item.path);
|
|
228
|
+
});
|
|
229
|
+
exiting.forEach((item) => action(item.state?.exit));
|
|
230
|
+
action(transitionRef.current?.actions);
|
|
231
|
+
|
|
232
|
+
entering.forEach((item) => {
|
|
233
|
+
const cleanup = effect(item.state?.effects);
|
|
234
|
+
if (!cleanup) return;
|
|
235
|
+
const existing = effects.current.get(item.path);
|
|
236
|
+
effects.current.set(item.path, existing ? callAll(existing, cleanup) : cleanup);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (previousState === INIT_STATE) {
|
|
240
|
+
action(machine.entry);
|
|
241
|
+
const cleanup = effect(machine.effects);
|
|
242
|
+
if (cleanup) {
|
|
243
|
+
const existing = effects.current.get(INIT_STATE);
|
|
244
|
+
effects.current.set(INIT_STATE, existing ? callAll(existing, cleanup) : cleanup);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
entering.forEach((item) => action(item.state?.entry));
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
subSlot(slot, 'state'),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const hydratedStateRef = useRef<string | undefined>(undefined, subSlot(slot, 'hydrated-state'));
|
|
255
|
+
const statusRef = useRef(MachineStatus.NotStarted, subSlot(slot, 'status'));
|
|
256
|
+
|
|
257
|
+
useSafeLayoutEffect(
|
|
258
|
+
() => {
|
|
259
|
+
let cancelled = false;
|
|
260
|
+
let didStart = false;
|
|
261
|
+
queueMicrotask(() => {
|
|
262
|
+
if (cancelled) return;
|
|
263
|
+
const started = statusRef.current === MachineStatus.Started;
|
|
264
|
+
didStart = true;
|
|
265
|
+
statusRef.current = MachineStatus.Started;
|
|
266
|
+
debug(started ? 'rehydrating...' : 'initializing...');
|
|
267
|
+
const initialState = hydratedStateRef.current ?? state.initial!;
|
|
268
|
+
state.invoke(initialState, started ? state.get() : INIT_STATE);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const functions = effects.current;
|
|
272
|
+
return () => {
|
|
273
|
+
cancelled = true;
|
|
274
|
+
const currentState = getCurrentState();
|
|
275
|
+
debug('unmounting...');
|
|
276
|
+
hydratedStateRef.current = currentState;
|
|
277
|
+
statusRef.current = MachineStatus.Stopped;
|
|
278
|
+
functions.forEach((fn) => fn?.());
|
|
279
|
+
effects.current = new Map();
|
|
280
|
+
transitionRef.current = null;
|
|
281
|
+
|
|
282
|
+
queueMicrotask(() => {
|
|
283
|
+
if (didStart) action(machine.exit);
|
|
284
|
+
statusRef.current = MachineStatus.Stopped;
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
[],
|
|
289
|
+
subSlot(slot, 'lifecycle'),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const getCurrentState = () =>
|
|
293
|
+
'ref' in state ? state.ref.current : (state as Bindable<string>).get();
|
|
294
|
+
|
|
295
|
+
const send = (event: any) => {
|
|
296
|
+
queueMicrotask(() => {
|
|
297
|
+
if (statusRef.current !== MachineStatus.Started) return;
|
|
298
|
+
|
|
299
|
+
previousEventRef.current = eventRef.current;
|
|
300
|
+
eventRef.current = event;
|
|
301
|
+
const currentState = getCurrentState();
|
|
302
|
+
const { transitions, source } = findTransition(machine, currentState, event.type as string);
|
|
303
|
+
const transition = choose(transitions);
|
|
304
|
+
if (!transition) return;
|
|
305
|
+
|
|
306
|
+
transitionRef.current = transition;
|
|
307
|
+
const target = resolveStateValue(machine, transition.target ?? currentState, source);
|
|
308
|
+
debug('transition', event.type, transition.target || currentState, `(${transition.actions})`);
|
|
309
|
+
|
|
310
|
+
if (target !== currentState) {
|
|
311
|
+
flushSync(() => state.set(target));
|
|
312
|
+
} else if (transition.reenter) {
|
|
313
|
+
state.invoke(currentState, currentState);
|
|
314
|
+
} else {
|
|
315
|
+
action(transition.actions ?? []);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
machine.watch?.(getParams());
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
state: getState(),
|
|
324
|
+
send,
|
|
325
|
+
context: ctx,
|
|
326
|
+
prop,
|
|
327
|
+
scope,
|
|
328
|
+
refs,
|
|
329
|
+
computed,
|
|
330
|
+
event: getEvent(),
|
|
331
|
+
getStatus: () => statusRef.current,
|
|
332
|
+
} as Service<T>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function useLiveRef<T>(value: T, slot: symbol) {
|
|
336
|
+
const ref = useRef(value, slot);
|
|
337
|
+
ref.current = value;
|
|
338
|
+
return ref;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function useProp<T>(value: T, slot: symbol) {
|
|
342
|
+
const ref = useLiveRef(value, slot);
|
|
343
|
+
return function get<K extends keyof T>(key: K): T[K] {
|
|
344
|
+
return ref.current[key];
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function flush(fn: VoidFunction) {
|
|
349
|
+
queueMicrotask(() => {
|
|
350
|
+
flushSync(() => fn());
|
|
351
|
+
});
|
|
352
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createNormalizer } from '@zag-js/types';
|
|
2
|
+
import type { JSX } from 'octane/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
export type PropTypes = JSX.IntrinsicElements & {
|
|
5
|
+
element: JSX.IntrinsicElements['div'];
|
|
6
|
+
style: Exclude<JSX.IntrinsicElements['div']['style'], string | undefined>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Match Octane's text-entry contract (native-change-diagnostics): only these
|
|
10
|
+
// input types treat React-style onChange as per-edit onInput. Everything in
|
|
11
|
+
// NON_TEXT_INPUT_TYPES keeps native onChange; unknown types follow HTML's
|
|
12
|
+
// invalid-value default and are treated as text.
|
|
13
|
+
const TEXT_ENTRY_TYPES = new Set(['email', 'number', 'password', 'search', 'tel', 'text', 'url']);
|
|
14
|
+
const NON_TEXT_INPUT_TYPES = new Set([
|
|
15
|
+
'button',
|
|
16
|
+
'checkbox',
|
|
17
|
+
'color',
|
|
18
|
+
'date',
|
|
19
|
+
'datetime-local',
|
|
20
|
+
'file',
|
|
21
|
+
'hidden',
|
|
22
|
+
'image',
|
|
23
|
+
'month',
|
|
24
|
+
'radio',
|
|
25
|
+
'range',
|
|
26
|
+
'reset',
|
|
27
|
+
'submit',
|
|
28
|
+
'time',
|
|
29
|
+
'week',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function isTextHostElement(elementKey: string, props: Record<string, unknown>): boolean {
|
|
33
|
+
if (elementKey === 'textarea') return true;
|
|
34
|
+
if (elementKey !== 'input') return false;
|
|
35
|
+
const type = typeof props.type === 'string' ? props.type.toLowerCase() : 'text';
|
|
36
|
+
if (TEXT_ENTRY_TYPES.has(type)) return true;
|
|
37
|
+
if (NON_TEXT_INPUT_TYPES.has(type)) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rewriteTextHostOnChange(
|
|
42
|
+
elementKey: string,
|
|
43
|
+
value: Record<string, unknown>,
|
|
44
|
+
): Record<string, unknown> {
|
|
45
|
+
if (!isTextHostElement(elementKey, value)) return value;
|
|
46
|
+
if (typeof value.onChange !== 'function' || value.onInput !== undefined) return value;
|
|
47
|
+
const { onChange, ...rest } = value;
|
|
48
|
+
return { ...rest, onInput: onChange };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ZagNormalizeProps = ReturnType<typeof createNormalizer<PropTypes>>;
|
|
52
|
+
|
|
53
|
+
function createOctaneNormalizer(): ZagNormalizeProps {
|
|
54
|
+
return new Proxy({} as ZagNormalizeProps, {
|
|
55
|
+
get(_target, key) {
|
|
56
|
+
if (key === 'style') {
|
|
57
|
+
return function styleProps(props: PropTypes['style']) {
|
|
58
|
+
return props;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return function normalizeElementProps(value: Record<string, unknown>) {
|
|
62
|
+
return rewriteTextHostOnChange(String(key), value);
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const normalizeProps = createOctaneNormalizer();
|
package/src/portal.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
createElement,
|
|
4
|
+
createPortal,
|
|
5
|
+
Fragment,
|
|
6
|
+
isChildrenBlock,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'octane';
|
|
10
|
+
import type { OctaneNode } from 'octane';
|
|
11
|
+
import { subSlot } from './internal';
|
|
12
|
+
|
|
13
|
+
export interface PortalProps {
|
|
14
|
+
disabled?: boolean | undefined;
|
|
15
|
+
container?: { current: HTMLElement | null } | undefined;
|
|
16
|
+
getRootNode?: (() => ShadowRoot | Document | Node) | undefined;
|
|
17
|
+
children?: OctaneNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PORTAL_SLOT = Symbol.for('@octanejs/zag/Portal');
|
|
21
|
+
|
|
22
|
+
export const Portal = (props: PortalProps) => {
|
|
23
|
+
const { children, container, disabled, getRootNode } = props;
|
|
24
|
+
const [mounted, setMounted] = useState(false, subSlot(PORTAL_SLOT, 'mounted'));
|
|
25
|
+
|
|
26
|
+
useLayoutEffect(
|
|
27
|
+
() => {
|
|
28
|
+
setMounted(true);
|
|
29
|
+
},
|
|
30
|
+
[],
|
|
31
|
+
subSlot(PORTAL_SLOT, 'mount-effect'),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (!mounted || typeof window === 'undefined' || disabled) {
|
|
35
|
+
return createElement(Fragment, null, children);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rootNode = getRootNode?.();
|
|
39
|
+
const doc =
|
|
40
|
+
rootNode?.nodeType === Node.DOCUMENT_NODE
|
|
41
|
+
? (rootNode as Document)
|
|
42
|
+
: (rootNode?.ownerDocument ?? document);
|
|
43
|
+
const mountNode = container?.current ?? doc.body;
|
|
44
|
+
if (isChildrenBlock(children)) {
|
|
45
|
+
return createPortal(children, mountNode);
|
|
46
|
+
}
|
|
47
|
+
const portals = Children.map(children, (child) => createPortal(child, mountNode));
|
|
48
|
+
return createElement(Fragment, null, portals);
|
|
49
|
+
};
|
package/src/refs.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useRef } from 'octane';
|
|
2
|
+
|
|
3
|
+
import { subSlot } from './internal';
|
|
4
|
+
|
|
5
|
+
export function useRefs<T>(refs: T, slot?: symbol) {
|
|
6
|
+
const ref = useRef(refs, subSlot(slot, 'refs'));
|
|
7
|
+
return {
|
|
8
|
+
get<K extends keyof T>(key: K): T[K] {
|
|
9
|
+
return ref.current[key];
|
|
10
|
+
},
|
|
11
|
+
set<K extends keyof T>(key: K, value: T[K]) {
|
|
12
|
+
ref.current[key] = value;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
package/src/track.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'octane';
|
|
2
|
+
|
|
3
|
+
import { subSlot } from './internal';
|
|
4
|
+
|
|
5
|
+
export const useTrack = (deps: any[], effect: VoidFunction, slot?: symbol) => {
|
|
6
|
+
const render = useRef(false, subSlot(slot, 'render'));
|
|
7
|
+
const called = useRef(false, subSlot(slot, 'called'));
|
|
8
|
+
|
|
9
|
+
useEffect(
|
|
10
|
+
() => {
|
|
11
|
+
const run = render.current && called.current;
|
|
12
|
+
if (run) return effect();
|
|
13
|
+
called.current = true;
|
|
14
|
+
},
|
|
15
|
+
[
|
|
16
|
+
...(deps ?? []).map((dependency) =>
|
|
17
|
+
typeof dependency === 'function' ? dependency() : dependency,
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
subSlot(slot, 'tracked-effect'),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
useEffect(
|
|
24
|
+
() => {
|
|
25
|
+
render.current = true;
|
|
26
|
+
return () => {
|
|
27
|
+
render.current = false;
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
[],
|
|
31
|
+
subSlot(slot, 'mount-effect'),
|
|
32
|
+
);
|
|
33
|
+
};
|