Skip to content

ZFIN-9566: Upgrade react#1806

Draft
rtaylorzfin wants to merge 3 commits intoZFIN:mainfrom
rtaylorzfin:zfin-9566
Draft

ZFIN-9566: Upgrade react#1806
rtaylorzfin wants to merge 3 commits intoZFIN:mainfrom
rtaylorzfin:zfin-9566

Conversation

@rtaylorzfin
Copy link
Copy Markdown
Collaborator

No description provided.

rtaylorzfin and others added 3 commits April 3, 2026 15:55
- 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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-dom to React 18 and add react-draggable as an explicit dependency.
  • Migrate React mounting from ReactDOM.render() to createRoot().render().
  • Introduce useFormLite and update form components/containers to import it instead of react-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.popover handler is registered inside an effect with an empty dependency array, so it closes over the initial isInitialized value. That makes the if (!isInitialized) guard unreliable and can lead to multiple createRoot() 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.

Comment on lines +21 to +32
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);

Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +48
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]);
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +30
const [meta, setMetaState] = useState({
isSubmitting: false,
isSubmitted: false,
isValid: true,
isTouched: false,
serverError: null,
});
const [fieldMeta, setFieldMetaState] = useState({});
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +152
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);
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +99 to 107
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);
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +7
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.
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +63
"react": "^18.2.0",
"react-dom": "^18.2.0",
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "18.2.0",
"react-dom": "18.2.0",

Copilot uses AI. Check for mistakes.
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.

2 participants