Skip to content

Conversation

@dirkwa
Copy link
Contributor

@dirkwa dirkwa commented Jan 17, 2026

Description:

This PR adds a new React 19 Admin UI alongside the existing legacy UI, allowing runtime switching via environment variable.

What Changed

  • New package@signalk/server-admin-ui-react19 with React 19, Zustand, and Vite
  • Legacy UI default@signalk/server-admin-ui
  • Runtime switching: Set SIGNALK_ADMIN_UI=react19 to use the new UI

Technical Details

Component Legacy UI React 19 UI
React 16.14.0 19.x
State Management Redux Zustand
Build Tool Vite Vite 6
Language JavaScript TypeScript
Bootstrap 4.x 5.x

React 19 UI Features

  • Zustand store - Unified state management with slices for app, WebSocket, data, and priorities
  • Vitest test suite - 86 tests covering all store slices
  • React Compiler - Automatic memoization
  • Granular subscriptions - Efficient real-time data updates

Usage

# Use legacy UI (default)
npm start

# Use React 19 UI
SIGNALK_ADMIN_UI=react19 npm start

No Impact

  • Server APIs (HTTP/WebSocket)
  • Plugin JavaScript APIs
  • Standalone webapps

@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 17, 2026

Updated user message:

image

@sbender9
Copy link
Member

Have you tried the signalk-shelly2 or bt-sensors plugins with this?

@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 17, 2026

Have you tried the signalk-shelly2 or bt-sensors plugins with this?

Hi Scott,

image

@dirkwa dirkwa force-pushed the react19 branch 2 times, most recently from 12df93b to 60e2032 Compare January 19, 2026 17:09
@dirkwa dirkwa changed the title Admin UI: React 19 Migration Admin UI: React 19.2 Migration with React Compiler Jan 19, 2026
@dirkwa dirkwa changed the title Admin UI: React 19.2 Migration with React Compiler Admin UI: TS refactor and React 19.2 Migration with React Compiler Jan 19, 2026
@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 19, 2026

@tkurki
This is now rebased on current master.
Using the Compiler brings quite some improvement in Data Browser.

@dirkwa dirkwa force-pushed the react19 branch 2 times, most recently from 2c23171 to 4c1a5e4 Compare January 24, 2026 04:25
@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 24, 2026

Same embedded Webapp - no code change - looks way nicer:

image

@bkeepers
Copy link
Contributor

I just sat down to work on this today and was very happy to see someone already did. Pulling down now to test and will do a review, but anything else I can do to help move this along?

@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 25, 2026

Pulling down now to test and will do a review, but anything else I can do to help move this along?

Thanks, looking forward to it. Indeed you could, if you want. As you can see here are some related PRs attached.

By design embedded Webapps (and just the embedded) fail installing in R19. The related PRs are hybrid installers for them, so after merging, their plugin installs and works on R16 and R19.

This is quite some lifting where AI can help perfectly.

@bkeepers
Copy link
Contributor

I did a bunch of click testing and everything is looking good. It's a huge diff, so I'm not sure how much value there is in manually reviewing, but I'll start looking through it and see how far I get.

@bkeepers
Copy link
Contributor

Naive question because I have not used Federated Modules: is there a reason that all plugins have to use the same version of React? I know running multiple is gross, but breaking the dependency between framework versions on the server and plugins would be a huge win in terms of keeping the server on a modern version and allowing plugins to update at their convenience.

@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 25, 2026

This is by design and JUST for embedded Webapps, there are only a few.

See discord discussion in channel Server about this. The breaking change would have been remove this feature.

@bkeepers
Copy link
Contributor

See discord discussion in channel Server about this. The breaking change would have been remove this feature.

I'm not able to find a link to this, so if you could share one that would be helpful. I usually try to link to any Discord discussions on pull requests if there's relevant context.

This is by design and JUST for embedded Webapps, there are only a few.

Was there discussion about allowing multple versions? If If there's not a technical reason to lock server and plugin versions, I think it would be very advantagous to decouple them.

@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 25, 2026

I'm not able to find a link to this, so if you could share one that would be helpful. I usually try to link to any Discord discussions on pull requests if there's relevant context.

https://discord.com/channels/1170433917761892493/1461248922201886772

Was there discussion about allowing multple versions? If If there's not a technical reason to lock server and plugin versions, I think it would be very advantagous to decouple them.

No - Teppo was willing to kill the support for embedded Webapps in favor to R19 - which I fully understand.
Latest when we move to R22 or so it will be again a big pain and the more embedded webapps there are the more painful it gets.

Nevertheless I found a way how embedded Webapp developers can make their plugin hybrid to happily exist in both worlds.
So I kept the embedded Webapp support, but see also the problems in the future if this eco system grows.

Migrate server-admin-ui from React 16 to React 19, including:
- React Router v4 to v6 (Switch→Routes, component→element)
- Redux 3 to Redux 5 with react-redux 9 hooks
- Bootstrap 4 to Bootstrap 5 class names (ml-*→ms-*, badge-*→text-bg-*)
- ReactDOM.render to createRoot API
- String refs to React.createRef()

Module Federation configured with singleton:true for React to ensure
hooks and context work correctly across host and plugin boundaries.
Plugins built against React 16 will need to be rebuilt.

Add improved error handling in dynamicutilities.js to detect React
version incompatibility and display user-friendly messages.

Add test plugin (examples/test-custom-renderer-plugin) demonstrating
React 19 compatible embedded webapp and custom Data Browser renderer.

BREAKING CHANGE: Embedded webapps and custom renderers built with
React 16 must be updated and rebuilt to work with React 19.
Migrate Admin UI components from legacy Redux connect() HOC pattern to
modern useSelector/useDispatch hooks. Convert affected files from
JavaScript to TypeScript for improved type safety.

Changes:
- Convert 12 component files from .js to .tsx with full TypeScript types
- Replace connect() mapStateToProps with useSelector hooks
- Replace connect() mapDispatchToProps with useDispatch hooks
- Add proper TypeScript interfaces for component props and state
- Remove unused eslint-disable comments for unconfigured rules
- Fix lint warnings for unused imports

Components converted to TypeScript with hooks:
- DataBrowser, Meta, Playground
- ProvidersConfiguration, ServerUpdate, Settings
- VesselConfiguration, Logging, BackupRestore, SourcePriorities
- Apps, OIDCSettings

Also includes fixes to existing hook-based components:
- Login.js, ServerLog.js: Remove obsolete eslint-disable comments
- Various security components: Modernize Redux usage
The component was importing appStore via connect() but never using it.
Remove old JavaScript files that were superseded by TypeScript conversions.
Delete unused routerCompat.tsx shim that was never imported.
Remove echo comments that simply describe what code does.

Update documentation to follow maintainer guidelines:
- Remove implementation details that may drift from code
- Replace inline code snippets with links to source files
- Update file references from .js to .tsx
- Remove @signalk/vesselpositions from optionalDependencies and categories.ts
- Remove vesselpositions.png image and references from webapps.md
- Fix sidebar submenu alignment for dropdown items without icons
- Remove echo comments and stale code cruft:
  - SidebarFooter/Form/Header: remove commented boilerplate
  - Sidebar.tsx: remove redundant function name comments
  - bootstrap.tsx: remove section header comments
  - appSlice.ts: remove echo comments
  - Dashboard.tsx: remove implementation history comment
  - _custom.scss: simplify comments, remove variable name echoes
- Fix SCSS deprecation warnings (PR 2237):
  - darken()/lighten() → color.adjust()
  - / division → math.div()
  - nth()/map-get()/map-keys() → list.nth()/map.get()/map.keys()
  - lightness() → color.channel()
  - Silence only @import deprecations (Bootstrap 5 still uses @import)
dirkwa added 18 commits January 26, 2026 05:55
Generated build artifact was accidentally committed, adding 44K lines.
Added to .gitignore to prevent future commits.
Enable strict type checking in server-admin-ui tsconfig.json and fix
all resulting type errors. Add graceful error handling for plugins
incompatible with React 19.

Key changes:
- Enable strict mode in tsconfig.json, update lib to ES2021
- Add missing properties and index signatures to store types
- Fix null/undefined handling with nullish coalescing operators
- Add type assertions for RJSF template compatibility
- Create type declaration for jsonlint-mod module
- Add PluginErrorBoundary to EmbeddedPluginConfigurationForm
- Enhance error detection in dynamicutilities.ts for React 19
  incompatibility patterns

Incompatible plugins now show a friendly warning instead of crashing
the entire configuration page.
Enable stricter TypeScript linting rules to catch unused code at
compile time and fix all resulting errors.

Changes:
- Enable noUnusedLocals and noUnusedParameters in tsconfig.json
- Remove unused React import from bootstrap.tsx
- Remove unused dispatch parameter from enableSecurity function
- Remove unused onClose prop from PluginConfigCard component
- Remove unused FormText import from Meta.tsx
- Remove unused Col and Row imports from ProvidersConfiguration.tsx
- Remove dead changelog fetching code from ServerUpdate.tsx
Fix missing status feedback when installing or removing apps in the
app store. The progress animation and status text (Installing, Removing,
Removed, etc.) were not displayed because the installing app properties
were not being merged into the derived app list.

Changes:
- Add missing properties to InstallingApp type (isRemoving, isRemove,
  installFailed)
- Spread all installing app properties in Apps.tsx deriveAppList
  function so ActionCellRenderer receives the status flags
Add WebappErrorBoundary to Embedded.tsx to gracefully handle React 19
incompatibility errors when loading webapps built for older React versions.

Previously, clicking on an incompatible webapp (e.g., @canboat/visual-analyzer)
crashed the entire admin UI with "Minified React error SignalK#306". Now users see
a friendly warning message with a "Try again" button, and can navigate away
without reloading the page.

The error boundary:
- Catches render-phase errors from dynamically loaded webapps
- Shows React 19 compatibility hint for known error patterns
- Provides retry functionality for transient errors
- Resets automatically when switching between webapps (via key prop)
Add container="body" to DropdownMenu in ActionCellRenderer to render
the dropdown in a portal, preventing it from being clipped by the
InfiniteScroll container's overflow.
Enable React Compiler 1.0 for automatic memoization and adopt the new
useEffectEvent hook for cleaner callback patterns.

React Compiler (babel-plugin-react-compiler):
- Automatically optimizes component rendering without manual memo()
- Removed memo() wrappers from 5 components: TimestampCell, DataRow,
  VirtualizedDataTable, CopyToClipboardWithFade, and ServerLog's LogRow

useEffectEvent in DataBrowser.tsx:
- Replaced ref-based pattern (updatePath$SourceKeysRef) with useEffectEvent
- The hook always sees latest state without causing effect re-runs
- Eliminates need for synchronization effects and reduces dependency arrays

Dependencies upgraded:
- react, react-dom: ^19.0.0 → ^19.2.0
- @types/react, @types/react-dom: ^19.0.0 → ^19.2.0
- Added babel-plugin-react-compiler: ^1.0.0
Resolves accessibility warnings about labels not being associated
with form fields in the DataBrowser component.
Remove redundant htmlFor from switch wrapper Labels that already
contain nested inputs. The text labels still retain htmlFor for
proper association.
- Update @vitejs/plugin-react from ^4.3.4 to ^5.1.2
  Fixes npm ERESOLVE error where 4.7.0 caused peer dependency conflicts

- Update @module-federation/vite from ^1.0.10 to ^1.9.4
  Version 1.0.10 no longer exists in npm registry

Both plugins remain necessary:
- @vitejs/plugin-react: Required for React Compiler (Babel-based)
- @module-federation/vite: Required for module federation support
Wrap source selection checkboxes in <label> elements to satisfy
browser accessibility checkers. The inputs already had aria-label
attributes for screen readers, but the DOM validator requires
a proper label association.
…ode virtualization

Add React 19.2 optimizations for better performance with many AIS targets:
- Add useDeferredValue for non-blocking search filtering
- Add useTransition for localStorage writes
- Memoize context dropdown options to prevent O(n) recalculation
- Increase structure debounce from 50ms to 200ms

Enable virtualization in RAW mode to reduce DOM size:
- Use 80px row height estimate for RAW rows (vs 40px normal)
- Apply content-visibility CSS for variable height handling
- Remove special case that rendered all rows in RAW mode

Add VirtualizedMetaTable component for Meta view virtualization.

Replace FontAwesome copy icons with CSS-based icons to reduce DOM nodes.

Make Sources card collapsible to reduce initial render.
Add eslint-plugin-react-hooks, eslint-plugin-react-compiler, and
@eslint-react/eslint-plugin for stricter React 19 compliance.

Update tsconfig.json with stricter compiler options: noImplicitReturns,
noImplicitOverride, allowUnusedLabels, allowUnreachableCode.

Fix lint violations properly throughout the codebase:
- Refactor prop mutation patterns to immutable state updates
- Replace array index keys with stable unique identifiers
- Fix missing useEffect dependencies
- Add proper button types and ref cleanup patterns
- Remove echo comments that just restate the code
The CSS grid layout with minmax() minimum widths summed to more than
narrow screen widths, causing columns to overlap on mobile devices.

Switch to a stacked card layout on screens narrower than 768px where
each row displays fields vertically with labels. Disable virtualization
on narrow screens since the stacked layout has variable row heights.

addresses SignalK#2281
The CSS grid layout with minmax() minimum widths summed to more than
narrow screen widths, causing columns to overlap on mobile devices.

Switch to a stacked card layout on screens narrower than 768px where
each row displays fields vertically with labels. Use larger row height
estimate (120px) for virtualization on narrow screens to account for
the taller stacked layout.
@bkeepers
Copy link
Contributor

Thanks for the context. Sorry to distract with discussions of a feature that probably should be deprecated.

Either way, I'm 100% in support of moving this PR forward and will help with any issues or bugs that come up.

@tkurki
Copy link
Member

tkurki commented Jan 25, 2026

Teppo was willing to kill the support for embedded Webapps in favor to R19

No, this is incorrect. Either i have been ambiguous or you have not quite heard me. I am willing to break the backwards compatibility to move to up to date React. Made peace with that decision already when I added the feature…

While supporting multiple reacts might be technically somehow possible it would be brittle and forfeit the advantage of NOT needing to load multiple Reacts.

Replace Redux with Zustand for unified state management across the admin UI.
This simplifies the codebase by using a single lightweight (~3KB) library
with better TypeScript support and React 19 compatibility.

Key changes:
- Add Zustand store with slices for app state, WebSocket, data, and priorities
- Route WebSocket delta messages directly to Zustand (remove ValueEmittingStore)
- Use useSyncExternalStore for throttled per-path subscriptions in DataBrowser
- Convert all components from useSelector/dispatch to useStore hooks
- Remove Redux dependencies (@reduxjs/toolkit, react-redux)

The WebSocket connection lifecycle and real-time data flow remain unchanged.
All existing functionality is preserved with improved performance characteristics
Add a new React 19 Admin UI (server-admin-ui-react19) alongside the
legacy React 16 Admin UI. Users can switch between them at runtime
using the SIGNALK_ADMIN_UI environment variable.

Changes:
- Add @signalk/server-admin-ui-react19 package with Zustand state
  management replacing Redux
- Restore legacy server-admin-ui from master as fallback
- Add runtime UI selection via SIGNALK_ADMIN_UI=react19 env var
- Add Vitest test suite for React 19 UI (86 tests for Zustand store)
- Update eslint.config.js to handle both UI packages
- Update typedoc.json to exclude both admin UI packages
- Add test:admin-ui script to root package.json

The legacy UI remains the default. Set SIGNALK_ADMIN_UI=react19 to use
the new React 19 UI.
@dirkwa dirkwa changed the title Admin UI: TS refactor and React 19.2 Migration with React Compiler feat: add React 19 Admin UI with runtime switching Jan 26, 2026
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.

4 participants