Conversation
- Update react and react-dom from 16.14.0 to 18.2.0 - Convert ReactDOM.render() to createRoot() in index.js and QuickFigure.js - Replace unmaintained react-form@4.0.1 (hard-pegged to React 16) with useFormLite.js — a lightweight drop-in replacement providing the same useForm/useField/splitFormProps API surface - Add react-draggable (missing transitive dep from @jbrowse) - Update 12 files to import from useFormLite instead of react-form Verified: webpack build succeeds, lint passes, tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Upgrades the frontend to React 18 and removes the unmaintained react-form dependency by introducing an in-repo “useFormLite” replacement and migrating React mount points to createRoot().
Changes:
- Upgrade
react/react-domto React 18 and addreact-draggableas an explicit dependency. - Migrate React mounting from
ReactDOM.render()tocreateRoot().render(). - Introduce
useFormLiteand update form components/containers to import it instead ofreact-form.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| reference/react-18-upgrade.md | Adds upgrade notes, compatibility matrix, and manual test checklist. |
| package.json | Bumps React deps to 18.x and adds react-draggable; removes react-form. |
| package-lock.json | Updates lockfile dependency graph for React 18 + related packages. |
| home/javascript/react/index.js | Switches app entry mounting from ReactDOM.render to createRoot().render. |
| home/javascript/react/hooks/useFormLite.js | Adds custom lightweight form library intended to replace react-form. |
| home/javascript/react/hooks/useAddEditDeleteForm.js | Switches useForm import to useFormLite. |
| home/javascript/react/containers/TranscriptEditAttributes.js | Switches useForm import to useFormLite. |
| home/javascript/react/containers/SequenceTargetingReagentEditSequence.js | Switches useForm import to useFormLite. |
| home/javascript/react/containers/QuickFigure.js | Migrates popover rendering from ReactDOM.render to createRoot().render. |
| home/javascript/react/containers/NewSequenceTargetingReagentForm.js | Switches useForm import to useFormLite. |
| home/javascript/react/containers/MarkerEditCloneData.js | Switches useForm import to useFormLite. |
| home/javascript/react/containers/AntibodyEditDetails.js | Switches useForm import to useFormLite. |
| home/javascript/react/components/marker-edit/MarkerPublicNoteForm.js | Switches useForm import to useFormLite. |
| home/javascript/react/components/marker-edit/MarkerNameForm.js | Switches useForm import to useFormLite. |
| home/javascript/react/components/marker-edit/EditOrthologyNote.js | Switches useForm import to useFormLite. |
| home/javascript/react/components/marker-edit/EditOrthologyEvidenceCell.js | Switches useForm import to useFormLite. |
| home/javascript/react/components/form/InputField.js | Switches useField/splitFormProps import to useFormLite. |
| home/javascript/react/components/form/InputCheckbox.js | Switches useField/splitFormProps import to useFormLite. |
Comments suppressed due to low confidence (1)
home/javascript/react/containers/QuickFigure.js:120
- The
inserted.bs.popoverhandler is registered inside an effect with an empty dependency array, so it closes over the initialisInitializedvalue. That makes theif (!isInitialized)guard unreliable and can lead to multiplecreateRoot()calls / renders. Use a ref for initialization state, or register/unregister the handler when dependencies change.
const handlePopoverInserted = () => {
if (!isInitialized) {
setIsInitialized(true);
const popoverEl = document.getElementById('popover-dialog-container');
popoverRootRef.current = createRoot(popoverEl);
const dialog = <QuickFigureDialog pubId={pubId} toggle={() => toggle()}/>;
popoverRootRef.current.render(dialog);
}
}
//set up the popover
useEffect(() => {
if (popoverRef.current) {
$(popoverRef.current).popover({
trigger: 'manual',
html: true,
content: () => popoverTemplate,
placement: 'bottom',
sanitize: false
}).on('inserted.bs.popover', handlePopoverInserted);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function useForm({ defaultValues = {}, onSubmit }) { | ||
| const [values, setValues] = useState({ ...defaultValues }); | ||
| const [meta, setMetaState] = useState({ | ||
| isSubmitting: false, | ||
| isSubmitted: false, | ||
| isValid: true, | ||
| isTouched: false, | ||
| serverError: null, | ||
| }); | ||
| const [fieldMeta, setFieldMetaState] = useState({}); | ||
| const defaultValuesRef = useRef(defaultValues); | ||
|
|
There was a problem hiding this comment.
useForm initializes state from defaultValues only on first render and keeps defaultValuesRef fixed, so forms that load defaults asynchronously (e.g., via useFetch) won’t populate with fetched data and reset() will revert to the initial empty defaults. Update the hook to react to defaultValues changes (and keep the reset baseline in sync).
| const setFieldValue = useCallback((field, value) => { | ||
| setValues(prev => ({ ...prev, [field]: value })); | ||
| setMetaState(prev => ({ ...prev, isTouched: true })); | ||
| }, []); | ||
|
|
||
| const setFieldMeta = useCallback((field, updates) => { | ||
| setFieldMetaState(prev => ({ ...prev, [field]: { ...(prev[field] || {}), ...updates } })); | ||
| }, []); | ||
|
|
||
| const getFieldValue = useCallback((field) => { | ||
| return values[field]; | ||
| }, [values]); |
There was a problem hiding this comment.
setFieldValue/getFieldValue treat field as a flat key; the codebase uses dotted paths like references.0.zdbID, which will not update nested structures and will break array/object form fields. Consider implementing path-based get/set (and ensure it works with arrays).
| const [meta, setMetaState] = useState({ | ||
| isSubmitting: false, | ||
| isSubmitted: false, | ||
| isValid: true, | ||
| isTouched: false, | ||
| serverError: null, | ||
| }); | ||
| const [fieldMeta, setFieldMetaState] = useState({}); |
There was a problem hiding this comment.
meta.isValid is initialized to true and never updated from field- or form-level validation results, so submit buttons gated by !isValid can become incorrectly enabled. Track validation errors (field + form validate) and derive/update meta.isValid accordingly.
| const setValue = useCallback((newValue) => { | ||
| setFieldValue(field, newValue); | ||
| setTouched(true); | ||
| // Run validation if provided | ||
| if (validateRef.current) { | ||
| const debounce = (fn, ms) => { | ||
| // Simplified: just run immediately for synchronous, schedule for async | ||
| setTimeout(() => { | ||
| Promise.resolve(fn()).then(result => { | ||
| setError(result || null); | ||
| }); | ||
| }, ms || 0); | ||
| }; | ||
| const result = validateRef.current(newValue, { debounce }); | ||
| if (result && typeof result.then === 'function') { | ||
| result.then(r => setError(r || null)); | ||
| } else if (typeof result === 'string') { | ||
| setError(result); | ||
| } else { | ||
| setError(null); | ||
| } |
There was a problem hiding this comment.
The provided debounce helper doesn’t cancel prior scheduled validations or guard against out-of-order async results, so older validations can overwrite newer ones. Also, debounce() currently returns undefined, which differs from how callers use it (they return debounce(async () => …, 300) from validate). Implement cancellable debouncing and ensure the return value matches caller expectations.
| const popoverRootRef = useRef(null); | ||
| const handlePopoverInserted = () => { | ||
| if (!isInitialized) { | ||
| setIsInitialized(true); | ||
| const popoverEl = document.getElementById('popover-dialog-container'); | ||
| popoverRootRef.current = createRoot(popoverEl); | ||
| const dialog = <QuickFigureDialog pubId={pubId} toggle={() => toggle()}/>; | ||
| ReactDOM.render(dialog, popoverEl); | ||
| popoverRootRef.current.render(dialog); | ||
| } |
There was a problem hiding this comment.
createRoot is created and rendered into the Bootstrap popover, but the React root is never unmounted when the popover/component is disposed. This can leak event handlers/state; call popoverRootRef.current?.unmount() during cleanup and when replacing the popover content.
| Upgraded React from 16.14.0 to 18.2.0. This is a major version jump (skipping 17) | ||
| but the codebase was in good shape — no deprecated lifecycle methods, no string refs, | ||
| no legacy context API. |
There was a problem hiding this comment.
This doc says React was upgraded to 18.2.0, but the lockfile currently resolves React/ReactDOM to 18.3.1 (because ^18.2.0 allows it). Either update the doc to match the resolved version, or pin the dependency to 18.2.0 if that’s the intended target.
| "react": "^18.2.0", | ||
| "react-dom": "^18.2.0", |
There was a problem hiding this comment.
react/react-dom are specified as ^18.2.0, which currently resolves to 18.3.1 in the lockfile. If you want the project to stay on 18.2.x (as the upgrade notes suggest), pin these to an exact version; otherwise update the upgrade notes to the actual resolved version.
| "react": "^18.2.0", | |
| "react-dom": "^18.2.0", | |
| "react": "18.2.0", | |
| "react-dom": "18.2.0", |
No description provided.