diff --git a/.changeset/resize-observer-solid2-migration.md b/.changeset/resize-observer-solid2-migration.md new file mode 100644 index 000000000..b4902e3db --- /dev/null +++ b/.changeset/resize-observer-solid2-migration.md @@ -0,0 +1,17 @@ +--- +"@solid-primitives/resize-observer": major +--- + +Migrate to Solid.js v2.0 (beta.13) + +## Breaking Changes + +**Peer dependencies**: `solid-js@^2.0.0-beta.13` and `@solidjs/web@^2.0.0-beta.13` are now required. + +### `@solid-primitives/resize-observer` + +- `isServer` now imported from `@solidjs/web` (not `solid-js/web`) +- `createResizeObserver`: internal `createEffect` converted to the Solid 2.0 split compute/apply pattern +- `createElementSize`: internal `createEffect` converted to split compute/apply pattern; element cleanup (`unobserve`) is returned from apply phase instead of using `onCleanup` +- `createElementSize`: `sharedConfig.context` replaced with `sharedConfig.hydrating` for hydration detection +- `createStore` setter in consuming code now requires a function argument (Solid 2.0 store API change) diff --git a/packages/resize-observer/README.md b/packages/resize-observer/README.md index 7eb188020..8cfa8d774 100644 --- a/packages/resize-observer/README.md +++ b/packages/resize-observer/README.md @@ -75,8 +75,8 @@ import { createResizeObserver } from "@solid-primitives/resize-observer"; let ref!: HTMLDivElement; -// can in onMount if the target variable isn't yet populated -onMount(() => { +// can use onSettled if the target variable isn't yet populated +onSettled(() => { createResizeObserver(ref, ({ width, height }, el) => { if (el === ref) console.log(width, height); }); @@ -95,10 +95,10 @@ createResizeObserver(targets, ({ width, height }, el) => {}); // updating the signal will unobserve removed elements and observe added ones setTargets(p => [...p, element]); -// createResizeObserver supports top-lever store arrays too +// createResizeObserver supports top-level store arrays too const [targets, setTargets] = createStore([document.body]); createResizeObserver(targets, ({ width, height }, el) => {}); -setTargets(targets.length, element); +setTargets(prev => [...prev, element]); ``` ## `createWindowSize` diff --git a/packages/resize-observer/package.json b/packages/resize-observer/package.json index ae69bcaee..017debb95 100644 --- a/packages/resize-observer/package.json +++ b/packages/resize-observer/package.json @@ -1,6 +1,6 @@ { "name": "@solid-primitives/resize-observer", - "version": "2.1.5", + "version": "3.0.0", "description": "Reactive primitives for observing resizing of HTML elements.", "author": "Moshe Udimar", "contributors": [ @@ -58,10 +58,12 @@ "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.13", + "solid-js": "^2.0.0-beta.13" }, "typesVersions": {}, "devDependencies": { - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.13", + "solid-js": "2.0.0-beta.13" } } diff --git a/packages/resize-observer/src/index.ts b/packages/resize-observer/src/index.ts index 3ed37d9de..7dc3ddafc 100644 --- a/packages/resize-observer/src/index.ts +++ b/packages/resize-observer/src/index.ts @@ -11,7 +11,7 @@ import { filterNonNullable, } from "@solid-primitives/utils"; import { type Accessor, createEffect, onCleanup, sharedConfig } from "solid-js"; -import { isServer } from "solid-js/web"; +import { isServer } from "@solidjs/web"; type ResizeObserverEntryGeneric = ResizeObserverEntry & { readonly target: T }; type ResizeObserverCallbackGeneric = ( @@ -88,11 +88,14 @@ export function createResizeObserver( } }, options); - createEffect((prev: T[]) => { - const refs = filterNonNullable(asArray(access(targets))); - handleDiffArray(refs, prev, observe, unobserve); - return refs; - }, []); + let prev: T[] = []; + createEffect( + () => filterNonNullable(asArray(access(targets))), + (refs: T[]) => { + handleDiffArray(refs, prev, observe, unobserve); + prev = refs; + }, + ); } const WINDOW_SIZE_FALLBACK = { width: 0, height: 0 } as const satisfies Size; @@ -177,21 +180,25 @@ export function createElementSize( const isFn = typeof target === "function"; const [size, setSize] = createStaticStore( - sharedConfig.context || isFn ? ELEMENT_SIZE_FALLBACK : getElementSize(target), + sharedConfig.hydrating || isFn ? ELEMENT_SIZE_FALLBACK : getElementSize(target), ); const ro = new ResizeObserver(([e]) => setSize(getElementSize(e!.target))); onCleanup(() => ro.disconnect()); if (isFn) { - createEffect(() => { - const el = target(); - if (el) { - setSize(getElementSize(el)); - ro.observe(el); - onCleanup(() => ro.unobserve(el)); - } - }); + createEffect( + () => target(), + (el: Element | false | null | undefined) => { + if (el) { + setSize(getElementSize(el)); + ro.observe(el); + return () => ro.unobserve(el); + } else { + setSize(ELEMENT_SIZE_FALLBACK); + } + }, + ); } else { ro.observe(target); onCleanup(() => ro.unobserve(target)); diff --git a/packages/resize-observer/test/index.test.ts b/packages/resize-observer/test/index.test.ts index ad703dbf6..ef20dffa4 100644 --- a/packages/resize-observer/test/index.test.ts +++ b/packages/resize-observer/test/index.test.ts @@ -1,6 +1,5 @@ import { describe, test, expect, afterAll } from "vitest"; -import { createRoot, createSignal, onMount } from "solid-js"; -import { createStore } from "solid-js/store"; +import { createRoot, createSignal, createStore, flush, onSettled } from "solid-js"; import { Size, createElementSize, @@ -26,6 +25,7 @@ afterAll(() => { let _targets: Set; let disconnect_count = 0; +let _observe_calls: Element[] = []; class TestResizeObserver { _targets: Set; constructor() { @@ -33,6 +33,7 @@ class TestResizeObserver { } observe(target: Element): void { this._targets.add(target); + _observe_calls.push(target); } unobserve(target: Element): void { this._targets.delete(target); @@ -64,6 +65,7 @@ describe("createResizeObserver", () => { createResizeObserver(div1, () => {}); return dispose; }); + flush(); expect(targets.size).toBe(1); expect(targets.has(div1)).toBeTruthy(); @@ -77,6 +79,7 @@ describe("createResizeObserver", () => { createResizeObserver([div1, div2], () => {}); return dispose; }); + flush(); expect(targets.size).toBe(2); expect(targets.has(div1)).toBeTruthy(); expect(targets.has(div2)).toBeTruthy(); @@ -92,12 +95,15 @@ describe("createResizeObserver", () => { expect(targets.size, "targets shouldn't be connected synchronously").toBe(0); return { dispose, setRefs }; }); + flush(); expect(targets.size).toBe(1); expect(targets.has(div1)).toBeTruthy(); setRefs([div2, div3]); + flush(); expect(targets.size).toBe(2); + expect(targets.has(div1)).toBeFalsy(); expect(targets.has(div2)).toBeTruthy(); expect(targets.has(div3)).toBeTruthy(); @@ -112,17 +118,93 @@ describe("createResizeObserver", () => { expect(targets.size, "targets shouldn't be connected synchronously").toBe(0); return { dispose, setRefs }; }); + flush(); expect(targets.size).toBe(1); expect(targets.has(div1)).toBeTruthy(); - setRefs([div2, div3]); + setRefs(() => [div2, div3]); + flush(); + expect(targets.size).toBe(2); + expect(targets.has(div1)).toBeFalsy(); + expect(targets.has(div2)).toBeTruthy(); + expect(targets.has(div3)).toBeTruthy(); + + dispose(); + }); + + test("observes added targets", () => { + const targets = (_targets = new Set()); + const { dispose, setRefs } = createRoot(dispose => { + const [refs, setRefs] = createSignal([div1]); + createResizeObserver(refs, () => {}); + return { dispose, setRefs }; + }); + flush(); + expect(targets.size).toBe(1); + expect(targets.has(div1)).toBeTruthy(); + + setRefs([div1, div2]); + flush(); expect(targets.size).toBe(2); + expect(targets.has(div1)).toBeTruthy(); + expect(targets.has(div2)).toBeTruthy(); + + setRefs([div1, div2, div3]); + flush(); + expect(targets.size).toBe(3); + expect(targets.has(div1)).toBeTruthy(); expect(targets.has(div2)).toBeTruthy(); expect(targets.has(div3)).toBeTruthy(); dispose(); }); + + test("unobserves removed targets", () => { + const targets = (_targets = new Set()); + const { dispose, setRefs } = createRoot(dispose => { + const [refs, setRefs] = createSignal([div1, div2, div3]); + createResizeObserver(refs, () => {}); + return { dispose, setRefs }; + }); + flush(); + expect(targets.size).toBe(3); + + setRefs([div1]); + flush(); + expect(targets.size).toBe(1); + expect(targets.has(div1)).toBeTruthy(); + expect(targets.has(div2)).toBeFalsy(); + expect(targets.has(div3)).toBeFalsy(); + + setRefs([]); + flush(); + expect(targets.size).toBe(0); + + dispose(); + }); + + test("observe is called only once per node", () => { + _targets = new Set(); + _observe_calls = []; + const { dispose, setRefs } = createRoot(dispose => { + const [refs, setRefs] = createSignal([div1, div2]); + createResizeObserver(refs, () => {}); + return { dispose, setRefs }; + }); + flush(); + expect(_observe_calls.length).toBe(2); + expect(_observe_calls.filter(t => t === div1).length).toBe(1); + expect(_observe_calls.filter(t => t === div2).length).toBe(1); + + _observe_calls = []; + setRefs([div1, div3]); + flush(); + expect(_observe_calls.length).toBe(1); + expect(_observe_calls.filter(t => t === div3).length).toBe(1); + + dispose(); + }); }); describe("getWindowSize", () => { @@ -158,7 +240,7 @@ describe("createElementSize", () => { expect(size.width).toBe(null); expect(size.height).toBe(null); - onMount(() => { + onSettled(() => { expect(size.width).toBe(100); expect(size.height).toBe(200); dispose(); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 278fdcaa7..236bd94db 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -3,9 +3,9 @@ import { onCleanup, createSignal, createStore, + createEffect, type Accessor, untrack, - type AccessorArray, type EffectFunction, type ComputeFunction, type NoInfer, @@ -15,10 +15,10 @@ import { type Store, type StoreSetter, sharedConfig, - onMount, DEV, - equalFn, } from "solid-js"; + +type AccessorArray = { readonly [K in keyof T]: Accessor }; // isServer moved from solid-js/web (1.x) to @solidjs/web (2.x). // typeof window is a universal fallback compatible with both versions. const isServer = typeof window === "undefined"; @@ -47,11 +47,11 @@ export const noop = (() => void 0) as Noop; export const trueFn: () => boolean = () => true; export const falseFn: () => boolean = () => false; -/** @deprecated use {@link equalFn} from "solid-js" */ -export const defaultEquals = equalFn; +/** @deprecated use reference equality `(a, b) => a === b` instead */ +export const defaultEquals = (a: unknown, b: unknown): boolean => a === b; export const EQUALS_FALSE_OPTIONS = { equals: false } as const satisfies SignalOptions; -export const INTERNAL_OPTIONS = { internal: true, ownedWrite: true } as const satisfies SignalOptions; +export const INTERNAL_OPTIONS = { ownedWrite: true } as const satisfies SignalOptions; /** * Check if the value is an instance of ___ @@ -183,7 +183,7 @@ export function defer( if (isArray) { input = Array(deps.length) as S; for (let i = 0; i < deps.length; i++) (input as any[])[i] = deps[i]!(); - } else input = deps(); + } else input = (deps as Accessor)(); if (shouldDefer) { shouldDefer = false; prevInput = input; @@ -266,7 +266,10 @@ export function createHydratableSignal( } if (sharedConfig.hydrating) { const [state, setState] = createSignal(serverValue as Exclude, options); - onSettled(() => { setState(() => update()); }); + createEffect( + () => {}, + () => { setState(() => update()); }, + ); return [state, setState]; } return createSignal(update() as Exclude, options); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 755314669..6628108a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -813,9 +813,12 @@ importers: specifier: workspace:^ version: link:../utils devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.13 + version: 2.0.0-beta.13(solid-js@2.0.0-beta.13) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.13 + version: 2.0.0-beta.13 packages/resource: devDependencies: