Skip to content

Migrate to React 19, Typescript, Vite, and Vitest - #5

Open
mrecachinas wants to merge 19 commits into
spectriclabs:mainfrom
mrecachinas:migrate-react-19
Open

mrecachinas wants to merge 19 commits into
spectriclabs:mainfrom
mrecachinas:migrate-react-19

Conversation

@mrecachinas

@mrecachinas mrecachinas commented Mar 12, 2026 •

Copy link
Copy Markdown

Migrates react-sigplot to React 19, TypeScript, Vite, and Vitest, and rewrites the class components as function components built on a shared useLayer hook.

Release: 1.0.0 (major)

This changes the public API, the sigplot dependency, and the package format, so it's a major release. Existing ^0.1.16 ranges stop below 0.2.0, so current users won't get it by accident.

Breaking changes since 0.1.16

Requirements

  • React 19 (react / react-dom ^19). prop-types is no longer a peer dependency.
  • sigplot ^3.1.7.
    • The old ^2.0.0-rc20 range actually installed 2.0.0-rc9, because npm sorts prerelease tags as strings.
    • Layer IDs from overlay_* are now UUID strings, and reload / push throw if given a numeric index. This only matters for code that calls the Plot API directly.

API

  • The Layer base class is removed; write function components with usePlot().
  • The Plugin base class is replaced by <Plugin plugin={instance} zorder={n} />.
  • Children of <SigPlot> no longer receive an injected plot prop; use usePlot().
  • A ref on <SigPlot> now receives the Plot itself (ref.current) instead of the component (ref.current.plot).
  • href (HrefLayer), wsurl (WebsocketLayer), and data (BlueLayer) are required.
  • BlueLayer replaces its layer when data changes, instead of calling reload.

Packaging

  • ESM (dist/react-sigplot.js) and CommonJS (dist/react-sigplot.cjs) builds with type declarations. The main path has changed.
  • The UMD build and window global are removed, since React 19 ships no UMD build.
  • An exports map blocks deep imports.
  • WPipeLayer is removed; it was never exported.

The README has a migration guide covering these.

Other changes

Fixes (also present in 0.1.16)

  • HrefLayer replaces only its own layer when href changes. Before, it removed every layer on the plot.
  • Removing a layer no longer breaks updates to its siblings, because layers are tracked by UUID instead of by position.
  • SigPlot removes sigplot's window / document listeners on unmount.
  • The canvas resizes before the browser paints, which fixes flicker while resizing.
  • ArrayLayer and PipeLayer accept typed arrays.
  • Options objects you pass in are no longer modified. sigplot deletes name from layer options, sets pipe on pipe headers, and fills in defaults on plot options, so it now gets a copy.
  • Inline options like options={{ autol: 1 }} no longer redo work on every render. Options were compared by reference, so each parent render re-applied them and redrew every plot; they're now compared by value. In a Chromium benchmark with 3 plots re-rendering at 60 Hz, this cut main-thread time from 183 ms/s to 39 ms/s.
  • Unmounted plots are released right away. Before, the last plots unmounted stayed in memory until React's next update, and sigplot's own module cache kept the newest plot.

New

  • <BoxesPlugin> wraps sigplot's boxes plugin:
    • onAdd / onRemove / onMove / onSelect callbacks, which receive copies of the boxes;
    • addOnMtag, which adds a box on ctrl/⌘-drag;
    • a ref for addBox / removeBox / clearBoxes / getBoxes.
  • onMtag on <SigPlot>, and usePlotEvent(type, handler) for any sigplot event.
  • PlotOptions, DataHeader, and LayerOptions list the settings sigplot 3.1.7 reads, with docs.
    • They note which plot settings sigplot only reads at creation or only when they change.
    • Settings that aren't listed are still accepted.
  • Types for every component and for the Plot API (Plot, LayerId, PlotPlugin, …).
  • usePlot() and SigPlotContext.
  • ref on <SigPlot>.

The boxes plugin, onMtag, and typed options are ported from #4 and updated for sigplot 3.x.

Tooling

  • TypeScript 7, Vite 6, Vitest 4, and Biome replace Babel, webpack, Jest, and ESLint/Prettier.
  • CI runs on main with Node 22, 24, and 26.
  • prepack builds dist/ before every publish.
  • Travis, jsdoc, and docgen configs are removed.

Testing

  • Lint, type-check (source, tests, and example), 69 tests (99% statement coverage), and build all pass.
  • @arethetypeswrong/cli finds no problems (node10, node16 CJS and ESM, bundler).
  • Installed the tarball from npm pack in a fresh project and checked:
    • CommonJS require
    • Node ESM import
    • a Vitest import
    • tsc with skipLibCheck: false, both bundler and nodenext resolution
  • Ran the demo (npm run dev) in Chromium and WebKit, and drew and moved boxes in Chromium.
  • In Chromium, mounted and unmounted 20 plots (with layers, boxes, and onMtag) three times. After forced garbage collection, 0 Plots and 0 canvases remained.

Known issues

  • Extra install size from sigplot. sigplot 3.x lists dev tools (budo, travis) as runtime dependencies. That adds about 300 packages to every install, including an elliptic advisory that has no fix. main's sigplot rc20 has the same problem; it needs a fix in sigplot.
  • Docs site. The GitHub Pages docs site no longer builds. The README is now the reference.
  • Package metadata. repository, homepage, and bugs in package.json still point to LGSInnovations/react-sigplot, the repo this one was forked from.

mrecachinas and others added 8 commits March 11, 2026 20:34
- Convert all class components to functional components with hooks
- Replace legacy Context API (childContextTypes) with React.createContext + usePlot hook
- Replace UNSAFE_componentWillReceiveProps with useEffect + useRef previous-props pattern
- Replace Layer base class inheritance with shared usePlot() hook
- Convert JavaScript to TypeScript with sigplot type declarations
- Replace webpack with Vite (library mode, ESM + UMD output)
- Replace Jest + Enzyme with Vitest + @testing-library/react
- Add vitest-canvas-mock with measureText fix for sigplot compatibility
- Update peer dependencies to React ^19.0.0
- Export SigPlotContext and usePlot hook for custom layer development
- Bump version to 1.0.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Run 'npm run dev' and open http://localhost:5173 to see the example.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add Plot cleanup on unmount (SigPlot.tsx) — removes injected canvases
  and nulls refs/state to prevent memory leaks and Strict Mode issues
- Fix multi-prop change drop in all layer update effects — use
  independent if blocks instead of if/else if chain so simultaneous
  prop changes are all processed
- Fix WPipeLayer: add wsurl default, remove non-null assertions,
  remove unused overrides prop from interface
- Guard change_settings calls against undefined props in all layers
- Fix PipeLayer initial mount to handle ArrayBuffer data (not just arrays)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- BlueLayer: 5 tests covering mount, reload, headermod, no-op, unmount
- Plugin: 2 tests covering add on mount, remove on unmount
- src/ coverage now at ~80% (all public API components 94-100%)
- Install @vitest/coverage-v8 for coverage reporting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mock overlay_href in all HrefLayer and SigPlot HrefLayer tests to
prevent sigplot from firing real XHR requests that fail in jsdom
with a DataView bounds error when parsing bluefile responses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add --no-warnings to vitest fork execArgv to suppress the harmless
Node 25 localstorage-file warning emitted by jsdom workers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mrecachinas mrecachinas changed the title Migrate react 19 Migrate to React 19, Typescript, Vite, and Vitest Mar 12, 2026
@mrecachinas
mrecachinas marked this pull request as ready for review March 12, 2026 01:33
mrecachinas and others added 11 commits March 16, 2026 20:09
- Add 'Migrating from 0.x' section documenting the Layer base class
  removal with before/after code examples showing usePlot() hook
- Document other breaking changes (React 19+, TypeScript types, Vite)
- Add 'npm run dev' to Development section

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add proper dependency arrays to all update effects (previously ran on every render)
- Split SigPlot's combined update effect into separate [height, width] and [options] effects, removing manual prev-ref tracking
- Plugin now uses [plot, plugin, pluginOptions] deps so React handles teardown/re-add on prop changes
- Wrap all layer and plugin components in React.memo to skip reconciliation when props are unchanged
- Add missing overrides change tracking in WebsocketLayer

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add @biomejs/biome for linting and formatting (singleQuote, 2-space indent)
- Add @typescript/native-preview (tsgo) for type-checking and declaration emit
- Remove typescript and prettier config from package.json
- Delete .eslintignore
- Add biome.json with recommended rules
- Add lint and format scripts
- Update build/typecheck scripts to use tsgo
- Replace eslint-disable comments with biome-ignore equivalents
- Auto-format codebase with biome

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Packaging
- Build ESM (.js) + CommonJS (.cjs) instead of UMD; `require()` of the
  old .umd.js crashed under "type": "module"
- Import sigplot's CJS default export so Node ESM/Vitest consumers can
  load the package
- Ship public Plot types (src/plot.ts) with ESM and CJS-scoped
  declarations; drop the leaked vitest.setup.d.ts
- Add prepack so `npm publish` builds dist/
- Replace the TypeScript nightly with typescript@7; type-check tests and
  example too

sigplot 3.x
- Depend on sigplot ^3.1.7. The old ^2.0.0-rc20 range resolved to
  2.0.0-rc9 because prerelease tags sort as strings
- Layer IDs are UUIDs, so removing a layer no longer invalidates others

Components
- Add a shared useLayer hook replacing six copies of lifecycle code
- HrefLayer: replace only its own layer on href change (rc9's
  deoverlay(undefined) wiped every layer)
- Plugin: pass `zorder` (sigplot's real 2nd arg) instead of options
- WebsocketLayer: reconnect only on wsurl change; copy overrides
- Array/PipeLayer: send the header with reload/push instead of an extra
  headermod; accept typed arrays
- BlueLayer: replace the layer on a new header instead of reload()
- SigPlot: call disable_listeners() on unmount, expose the Plot via
  ref, skip redundant change_settings/checkresize on mount
- Remove unexported WPipeLayer

Tooling
- Vitest 4 (fixes GHSA-82fw-gwwq-j7x9), parallel tests, narrow
  webstorage flag instead of --no-warnings
- Drop unused canvas and jest-canvas-mock
- CI on main with Node 22/24/26; remove Travis, jsdoc, and docgen
  configs

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
SigPlot applied size changes with checkresize() in a passive effect, which
React may run after the browser has painted the resized container. The
canvas then lags its container by a frame (measured: 49% of frames at 4x
CPU throttle). Running it in a layout effect resizes the canvas before
paint, and sigplot's rAF redraw still lands in the same frame (measured:
0 stale and 0 blank frames).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WebKit-based browsers block cleartext HTTP to remote hosts, so the
HrefLayer demo plot stayed empty there. The file is served over HTTPS
with CORS.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
sigplot re-autoscales a 1-D pipe only on pushes that carry a header.
0.x and the original PR passed `options` with every push; 3598621 sent it
only when it changed, so after the first frame the Y axis froze with
autol <= 1 and crept with the default autol=100 (19 of 100 after 20
frames). Restore the 0.x behavior.

Also document in the migration guide that children no longer receive an
injected `plot` prop.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Node 20 exits with "bad option" for the flag, which crashed every Vitest
worker even though Vitest 4 supports Node 20.19+. Detect support via
process.allowedNodeEnvironmentFlags, and add @types/node for the config
while keeping Node globals out of the library's declaration build.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Ports the ideas from spectriclabs#4 (by @dselck), rebuilt for sigplot 3.x:

- <BoxesPlugin> wraps sigplot's boxes plugin: onAdd/onRemove/onMove/
  onSelect receive copies of the plugin's live box objects, addOnMtag
  adds a box on ctrl/cmd-drag (ignoring plain clicks, which spectriclabs#4 turned
  into zero-size boxes), option changes apply to existing boxes, and a
  ref exposes addBox/removeBox/clearBoxes/getBoxes. boxselect carries
  `boxes` in sigplot 3.x, not `box` as spectriclabs#4 assumed.
- <SigPlot onMtag> and a general usePlotEvent(type, handler) hook, which
  keeps the latest handler without re-subscribing and removes listeners
  on unmount.
- PlotOptions, DataHeader, and LayerOptions now list the keys sigplot
  3.1.7 reads, with docs, and mark plot settings sigplot reads only at
  creation or only on change. They keep an index signature, so unlisted
  keys still type-check. Several of spectriclabs#4's types were wrong for 3.x
  (rubberbox_mode, nospec, origin).

Also copy options before handing them to sigplot, which deletes `name`
from layer options and sets `pipe` on pipe headers, mutating the
caller's objects (0.x had the same problem).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Performance: layers and SigPlot compared options objects by reference,
so inline `options={{ ... }}` counted as changed on every parent render.
Each render then ran headermod/change_settings/push and redrew every
plot. In a Chromium benchmark (3 plots, 60 Hz re-renders, no data
changes) that was 6 change_settings, 3 headermod, and 3 push calls per
render and 183 ms/s of main-thread time, versus 40 ms/s with stable
options. Plain-object props are now compared by value; data, callbacks,
and headers still by reference. After the fix both cases make 0 sigplot
calls and cost the same.

That required giving sigplot copies of every options object, since it
normalizes them in place (the Plot constructor adds xlab/ylab,
change_settings fills colors.fg/bg, layers set maxhold.decay). copyOf is
now a deep copy of plain objects and arrays, except user_data, which
sigplot stores on the layer by reference.

Memory: after unmounting, the last batch of plots stayed alive (20 of
20 in a mount/unmount test).
- setPlot(null) / setPlugin(null) in unmount cleanups queued state
  updates that React held, with the Plot, until its next render.
- sigplot's mx.open caches the newest plot's 2D context in a module
  global (mx._ctx) it never reads. SigPlot now clears it once its canvas
  has left the page.
After both fixes, 0 Plots and 0 canvases survive an unmount.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Gives coding agents the commands CI runs, the source layout, and the
non-obvious rules behind the code: sigplot mutates its inputs (copyOf),
options are compared by value (propChanged), layer IDs are UUIDs, teardown
must not setState(null), and the jsdom canvas measureText patch.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant