Reference Implementation: Rails Frontend (Legacy UI)¶
Overview¶
The Nextpoint frontend is a server-rendered Rails application with selective React components for interactive features. It runs a dual JavaScript pipeline: Sprockets (legacy jQuery/AG Grid) alongside Webpacker (React 18). The app is ~97% server-rendered ERB with React mounted into specific containers.
Architecture¶
rails/
├── app/javascript/ # Webpacker (React 18)
│ ├── packs/
│ │ ├── application.js # Main entry point
│ │ └── components.js # React bridge via react_ujs + createRoot
│ ├── components/ # ~105 JSX files (atomic design)
│ │ ├── atoms/ # 21 — Button, Badge, Checkbox, Spinner, Tooltip, etc.
│ │ ├── molecules/ # 17 — BatesStampModal, CustomAnnotationSidebar, charts
│ │ ├── organisms/ # 25 — Batch details (12), doc viewer toolbars (7), imports (5)
│ │ ├── containers/ # 10 — SearchTermsGrid, report modals, bulk actions
│ │ ├── pages/ # 5 — BatchDetailsPage, ImportsPage, SplitBatchDetailsPage
│ │ ├── templates/ # 4 — CardContainer, EditorModal, ModalContainer, PageContainer
│ │ ├── utils/ # 10 — Annotation helpers, PDF utilities, stamp utilities
│ │ ├── api/ # 7 — Fetch wrappers for viewer, stamps, imports
│ │ ├── DocumentViewPdf.jsx # Read-only PDF viewer (Legacy cases)
│ │ ├── NgeDocumentViewPdf.jsx # Full NGE PDF editor (~52KB, Nutrient/PSPDFKit)
│ │ ├── DocumentToolbar.jsx # Document toolbar (~74KB, largest component)
│ │ ├── AnalyticsTab.jsx # Analytics dashboard
│ │ └── ExhibitStampEditor.jsx # Stamp editing
│ └── channels/ # Action Cable (WebSocket)
├── app/assets/
│ ├── javascripts/ # Sprockets (legacy, 156 .js files)
│ │ ├── documents/ # Document grid, coding panel, viewer bridge
│ │ │ ├── documents_grid.js # Main document grid (AG Grid)
│ │ │ ├── document_view_pdf.js # Legacy viewer bridge → React
│ │ │ └── coding_panel.js # Coding/review panel
│ │ ├── chatbot.js # AI chatbot
│ │ ├── ai_assistant.js # AI assistant
│ │ ├── chronology/ # Chronology/timeline
│ │ ├── image_markups/ # Markup tools
│ │ ├── case_folder_uploader.js # S3 upload
│ │ └── vendor/ # jQuery 2.2.2, AG Grid Enterprise, Handlebars, etc.
│ └── stylesheets/ # 85 plain CSS files (no Sass/SCSS)
│ ├── components/ # Atomic: atoms/, molecules/, organisms/, pages/
│ ├── application.css # Sprockets manifest
│ └── vendor/ # jQuery UI, IcoMoon, Select2, AG Grid themes
├── app/views/ # 914 ERB templates across 101 directories
└── package.json # React 18, AG Grid 30, Webpacker 5.4.4, Webpack 4
Dual JavaScript Pipeline¶
Sprockets (Legacy — 156 files)¶
The original JS pipeline, still serving the majority of the application:
| Framework | Version | Usage |
|---|---|---|
| jQuery | 2.2.2 | DOM manipulation, AJAX, event handling |
| jQuery UI | — | Dialogs, datepickers, drag/drop, sortable |
| AG Grid Enterprise | vendored | Document grid (main review interface) |
| Handlebars | 4.7.7 | Client-side templating for grid cells |
| Moment.js | — | Date formatting |
| Select2 | — | Enhanced dropdowns |
| Flatpickr | — | Date pickers |
| DataTables | 1.11.3 | Tabular data (non-grid views) |
| SpreadJS | — | Spreadsheet viewer |
| PapaParse | — | CSV parsing |
| jsPDF + pdfmake | — | Client-side PDF generation |
| html2canvas | — | Screenshot capture |
| DOMPurify | — | XSS sanitization |
Webpacker (React 18 — ~105 JSX files)¶
Modern React components mounted into ERB via react_component helper:
| Package | Version | Usage |
|---|---|---|
| React / React DOM | ^18.3.1 | UI framework |
| react_ujs | ^2.2.1 | Rails-React bridge (auto-mount) |
| react-router-dom | ^6.26.2 | Client routing (batch details only) |
| AG Grid React | ^30.2.1 | React grid wrapper |
| Chart.js + react-chartjs-2 | ^4.4.6 | Analytics charts |
| axios | ^1.7.9 | HTTP client |
| react-select | ^5.10.2 | Enhanced dropdowns |
| react-hot-toast | ^2.5.2 | Toast notifications |
React-ERB Integration Pattern¶
ERB Template React Component
───────────── ───────────────
<%= react_component( class MyComponent extends React.Component {
"MyComponent", render() { return <div>...</div> }
{ prop1: @data } }
) %>
│ ▲
▼ │
<div data-react-class="MyComponent" react_ujs scans DOM on page load,
data-react-props='{"prop1":...}' finds data-react-class attrs,
/> mounts with createRoot (React 18)
Key patterns:
- ERB → React: react_component("Name", {props}) in ERB templates
- Global bridges: window.NP (legacy config/data) and window.nextpoint (React viewer APIs)
- Legacy → React reload: window.nextpoint.reloadReactViewer() re-mounts after jQuery DOM changes
- Server data bootstrap: Rails controllers pass data as React props OR as window.NP.bootstrapped_* globals
- No SPA: React Router only in batch details (BatchDetailsRouter.jsx). All other navigation is full-page ERB.
React Component Categories¶
Document Viewer (NGE vs Legacy — the biggest divergence)¶
| Component | Size | Purpose |
|---|---|---|
NgeDocumentViewPdf.jsx |
~52KB | Full NGE editor: Nutrient/PSPDFKit, JWT auth, annotations, stamps |
DocumentViewPdf.jsx |
Smaller | Read-only Legacy viewer: client-side PDF load, search, toolbar |
DocumentToolbar.jsx |
~74KB | Toolbar: page ops, markups, export, processing_in_nge locking |
Nutrient/PSPDFKit integration:
- Loaded from CDN (useCDN: true), licensed via NP.nutrient_license_key
- NGE mode: server-backed via JWT (nutrient_jwt_controller.rb), AutoSaveMode.IMMEDIATE
- Legacy mode: client-side read-only PDF loading
- Legacy bridge: document_view_pdf.js (Sprockets) calls window.nextpoint.DocumentViewPdf.init()
Batch/Import (React pages)¶
| Component | Purpose |
|---|---|
BatchDetailsPage.jsx |
Batch detail view with router |
ImportsPage.jsx |
Import configuration |
SplitBatchDetailsPage.jsx |
Split batch view |
| 12 organism components | Batch tabs, events, settings, progress |
Search/Analytics¶
| Component | Purpose |
|---|---|
AnalyticsTab.jsx |
Analytics dashboard with Chart.js |
SearchTermsGrid.jsx |
Search terms management (AG Grid) |
Coding/Stamps¶
| Component | Purpose |
|---|---|
ExhibitStampEditor.jsx |
Bates/confidentiality stamp editing |
BatesStampModal.jsx |
Stamp configuration modal |
CustomAnnotationSidebar.jsx |
Annotation sidebar |
CSS Architecture¶
- 85 plain CSS files — no Sass, no SCSS, no CSS-in-JS, no Tailwind
- Sprockets manifest (
application.css) pulls in all files - Component CSS follows atomic design:
components/atoms/,molecules/,organisms/,pages/ - AG Grid themes:
ag-theme-alpine+ag-grid-balham - NGE vs Legacy body class:
<body class="nge">or<body class="legacy">enables global CSS targeting
NGE vs Legacy UI Summary¶
| Aspect | NGE | Legacy |
|---|---|---|
| Document viewer | NgeDocumentViewPdf.jsx (Nutrient, JWT, annotations) |
DocumentViewPdf.jsx (read-only PDF) |
| Page thumbnails | Hidden (Nutrient renders pages) | S3 page images shown |
| Toolbar | DocumentToolbar.jsx with processing_in_nge locking |
Standard toolbar |
| Body class | nge |
legacy |
| Import flow | React ImportsPage + different format options |
ERB-based multi-step |
| Batch details | React BatchDetailsPage with Athena event tracking |
ERB with Legacy event display |
| Wire/Exchange | Hidden | jQuery-based wire workflow |
Key Architectural Observations¶
- ~97% server-rendered — React is an overlay, not a replacement. Most pages are ERB.
- No TypeScript — all React is plain JSX with prop-types
- No test framework for frontend — no Jest, no React Testing Library in package.json
- Two grid systems — AG Grid Enterprise (vendored in Sprockets) AND AG Grid React (via npm)
- Largest components are NGE-related —
DocumentToolbar.jsx(74KB) andNgeDocumentViewPdf.jsx(52KB) - jQuery and React coexist — jQuery handles the document grid, coding panel, chronology, markups; React handles batch details, document viewer, analytics
- Global state via window — no Redux, no Context API for cross-component state.
window.NPis the global data store.
Modernization Plan¶
See ADR-004: Incremental Frontend Modernization for the full decision record.
Strategy: Grow React From the Inside Out — No Big Rewrite¶
The frontend modernization follows an incremental approach with zero dedicated rewrite sprints. The key insight: 914 ERB templates are fine. Server-rendered HTML works for ~90% of Nextpoint's pages. React should only exist where rich client interactivity is needed.
Phase 0: Foundation (1-2 sprints, one engineer)¶
Replace the build toolchain without changing application code:
1. Replace Webpacker 5 → jsbundling-rails (esbuild)
- Webpacker 5 / Webpack 4 are EOL (no security patches)
- esbuild builds in <1 second vs Webpack's 30+ seconds
- Rails 7 officially recommends jsbundling-rails
- Build-tool swap only — zero React component code changes
2. Add TypeScript (tsconfig.json with allowJs: true)
- Zero files need to change on day 1
- New files → .tsx, existing files stay .jsx
- Rename .jsx → .tsx when touching a file for other reasons
Phase 1: Safety Net (ongoing, no extra sprints)¶
Boy Scout Rule — leave code better than you found it:
When fixing a bug in a React component → add a test for that bug
When adding a feature to a React component → add TypeScript to that file
When touching CSS → no changes needed (plain CSS works)
No dedicated "add tests" or "add TypeScript" sprints.
Phase 2: New Features (ongoing, natural)¶
New interactive feature → React component (mount in ERB via react_component)
New settings/admin page → ERB (server-rendered is fine)
New data visualization → React + Chart.js
Phase 3: Convert on Contact (opportunistic, no deadline)¶
When a jQuery page needs significant rework, convert to React. Priority order:
| Priority | Page | Reason |
|---|---|---|
| 1 | Coding panel | Heavy state, frequent feature requests, most dev friction |
| 2 | Import wizard | Already partially React, natural to complete |
| 3 | Export configuration | Forms + validation, benefits from React state |
| 4 | Document grid | LAST — huge, working, AG Grid handles it well |
What NOT to Do¶
| Avoid | Reason |
|---|---|
| Rewrite all jQuery to React | Massive effort, zero user value, high regression risk |
| Add Redux/Zustand | React Context suffices for component tree size |
| Adopt Next.js/Remix/SPA | Would require API layer that doesn't exist |
| Component library upfront | Build as you go, extract at ≥3 uses |
| Set deadline for modernization | Permanent practice, not a project |
| Rewrite ERB to React | Server-rendered HTML is correct for 90% of pages |
When to Use What¶
ERB: Static content, simple forms, read-only pages, admin panels
React: Rich interactivity, drag/drop, real-time updates, complex state, data viz
jQuery: It works and nobody is actively changing it (don't fix what isn't broken)
Estimated Cost¶
Phase 0: 1-2 sprints (one engineer) — build tool swap + tsconfig
Phase 1: 0 extra time — part of normal bug fixes
Phase 2: 0 extra time — part of normal feature work
Phase 3: Variable — only when touching those pages anyway
Key File Locations¶
| File | Purpose |
|---|---|
app/javascript/packs/application.js |
Webpacker entry point |
app/javascript/packs/components.js |
React bridge with createRoot |
app/javascript/components/NgeDocumentViewPdf.jsx |
NGE PDF editor (Nutrient) |
app/javascript/components/DocumentViewPdf.jsx |
Legacy PDF viewer |
app/javascript/components/DocumentToolbar.jsx |
Main toolbar (74KB) |
app/assets/javascripts/documents/documents_grid.js |
AG Grid document grid |
app/assets/javascripts/documents/coding_panel.js |
Review coding panel |
app/assets/javascripts/documents/document_view_pdf.js |
Legacy → React viewer bridge |
app/views/layouts/application.html.erb |
Main layout (body class nge/legacy) |
config/webpacker.yml |
Webpacker config |
package.json |
JS dependencies |
Ask questions about Nextpoint architecture, patterns, rules, or any module. Powered by Claude Opus 4.6.