import { type Accessor, createEffect, untrack } from "solid-js"; /** * Fires `onChange(next, previous)` whenever the tracked value changes by * identity. Mirrors Base UI React's `useValueChanged` * (`packages/react/src/internals/useValueChanged.ts`). * * The value must be provided as a reactive accessor. In Solid 2, change * detection maps naturally to the split-effect form: the compute half returns * the current value, and the apply half receives both the new compute return * and the prior one. We carry a "first run" latch so the callback isn't * fired on initial setup (matching React — its first effect pass sees * `valueRef.current === value` because both came from the same render). * * The callback runs inside `untrack` so it can call into other accessors * (`field.shouldValidateOnChange()`, `name()`, etc.) without tripping * `[STRICT_READ_UNTRACKED]`. Side-effect callbacks read snapshots, they * don't subscribe — re-runs are driven by the tracked `value` accessor * passed as the first argument. * * @param value Accessor for the tracked value. * @param onChange Called with `(next, previous)` whenever `value()` changes. * Most callers ignore `previous` — accept it positionally if * you need to compare the transition. */ export function useValueChanged( value: Accessor, onChange: (next: T, previous: T) => void, ): void { // Closure latch — flipped true after the effect's first apply so // subsequent runs fire `onChange`. Declared before `createEffect` so the // apply closure doesn't reference a let-binding that is textually below // its own declaration (TDZ-safe today because apply is async, but it's // fragile — keeping the declaration above is clearer). let primed = false; createEffect( // The compute phase just tracks `value()`; its return is also what // Solid forwards as `prev` on the next run. () => value(), (next, prev) => { // `prev` is undefined on the first run (Solid 2 split-effect // semantics). But `undefined` is a legitimate `T` value for some // callers, so we use a separate latch instead of checking `prev`. if (!primed) { primed = true; return; } if (!Object.is(next, prev as T)) { untrack(() => onChange(next, prev as T)); } }, ); }