TL;DR "findDOMNode is not a function" on React 19 means a dependency still calls ReactDOM.findDOMNode, which React 19 removed. Find the package in the stack trace (or grep node_modules), then replace it or patch it to read a ref. Pinning React 18 only works outside Next.js 15. Tested errors for seven popular old packages, with a replacement for each.
findDOMNode is not a function on React 19 means one of your dependencies still calls ReactDOM.findDOMNode, and React 19 removed it. Your own code is rarely the cause. The fix is to find which package makes the call and replace it, or patch it to use a ref. Pinning React 18 also works, but Next.js 15 needs React 19, so that only buys time.
I make qodesign, animated React and Tailwind components for Next.js. While checking which old packages still work on React 19, we rendered each one with React 19.2.9 in a scratch project, and some again with React 18.3.1. The errors in the table below are the exact ones we got, and the fixes link to our guide for each package.
findDOMNode took a class component instance and returned its DOM node. React deprecated it in October 2018 (v16.6.0), and the React 19 upgrade guide lists it under "Removed: ReactDOM.findDOMNode". The guide gives the reasons: it was slow, fragile to refactoring, only returned the first child and broke abstraction levels.
React 19's react-dom simply doesn't export it any more. A package that does ReactDOM.findDOMNode(this) reads undefined and calls it, so you get TypeError: findDOMNode is not a function. The prefix depends on how the package was bundled. react-input-mask gave us reactDom.findDOMNode is not a function, and bug reports for other packages show react_dom_1.default.findDOMNode or u(...).findDOMNode. They're all the same error.
npm doesn't always warn you first. react-input-mask, for one, declares react >=0.14.0, which React 19 satisfies, so the install goes through and the crash only shows up when the component mounts.
Credit Card Checkout V1 running live. A live 3D card that fills in as you type and flips for the CVC.
The stack trace almost always names it. Look for the first line with node_modules/<package>/ in the path. In Next.js dev mode the error overlay shows the call stack. Library frames can be collapsed there, so expand them if you don't see a node_modules path.
If the trace only shows bundled chunk names, list every installed package whose code mentions findDOMNode:
That prints the top-level package that carries the call, so a match inside node_modules/a/node_modules/b shows up as a. A package that only mentions the name in an error message or a guarded fallback shows up too, so treat the list as suspects and let the stack trace decide. Then npm ls <package> tells you which of your own dependencies pulled it in.
Every row is a package we tested on October 26, 2026 in jsdom with react-dom/client. "Throws" means it threw on React 19.2.9. Where we re-ran it on React 18.3.1, it worked there.
Package (version)
What React 19.2.9 did
What to use instead
react-input-mask 2.0.4
Threw reactDom.findDOMNode is not a function and rendered nothing. Rendered on React 18.3.1.
Only the first two are findDOMNode errors. The others come from APIs React 19 removed in the same release (ReactDOM.render, string refs, legacy context), and they show up the same way: an old package, a fresh React 19 app and a crash on mount.
Not every old package breaks. These rendered on React 19.2.9 in the same test: react-text-mask 5.5.0, react-headroom 3.2.1, react-sparklines 1.7.0, react-card-flip 1.2.3, react-toggle 4.1.3, react-google-login 5.2.2, and react-flip-move 3.0.5 on first mount (we didn't test reordering). Some of them still have peer ranges that stop at React 18, so npm asks for --legacy-peer-deps to install them, but once installed they rendered.
If your package isn't in the table, search its issue tracker for "React 19" or "findDOMNode" before you decide. If it's still maintained, a newer major may already have the fix.
For an archived or unmaintained library this is the real fix. The links in the table go to our replacement guide for each one, and most need nothing paid.
If the package is small and you can't drop it yet, change the call yourself (for example with patch-package, or by copying the file into your project). The React docs give the pattern: put a ref on the element and read ref.current where the code used to call findDOMNode(this).
tsx
import { Component, createRef } from "react";// Before (React 18 only):// componentDidMount() { (findDOMNode(this) as HTMLInputElement).select(); }// After: give the element a ref and read it.export class AutoselectInput extends Component<{ defaultValue?: string }> { input = createRef<HTMLInputElement>(); componentDidMount() { this.input.current?.select(); } render() { return <input ref={this.input} defaultValue={this.props.defaultValue} />; }}
When the library used findDOMNode on children you pass in, so it has no element of its own to put a ref on, the React docs' third alternative is to wrap the children in a div the library owns. Measure the first element inside that div, which is the node findDOMNode(child) used to return. Measuring the div itself doesn't work: a block div is as wide as its parent, so its width doesn't follow the child. We ran the component below in Chromium with React 19.1.1: it reported the child's size, then reported again when the child grew wider and taller. The wrapper adds an element to your layout, so check your CSS after.
tsx
"use client";import { useLayoutEffect, useRef, type ReactNode } from "react";// A library that called findDOMNode(child) to measure whatever you passed in// can wrap the children in a div it owns and measure that instead.export function Measure({ children, onSize }: { children: ReactNode; onSize: (rect: DOMRect) => void }) { const box = useRef<HTMLDivElement>(null); useLayoutEffect(() => { // findDOMNode(child) returned the child's first DOM node, so measure that. // The div itself is as wide as its parent, so its own size wouldn't follow the child. const el = box.current?.firstElementChild ?? box.current; if (!el) return; const ro = new ResizeObserver(() => onSize(el.getBoundingClientRect())); ro.observe(el); return () => ro.disconnect(); }, [onSize]); return <div ref={box}>{children}</div>;}
In your own function components there's nothing to patch: on React 19 ref is a normal prop, so you can pass it straight to the element without forwardRef.
react-input-mask is the biggest package in the table, and a lot of its use is formatting card numbers. If that's your case, you may not need a mask at all. Stripe's card fields format the number, expiry and CVC themselves, and the digits never reach your code.
If you want the card form to look finished as well, our Credit Card Checkout V1 is a 3D card that fills in as you type and flips for the CVC, with Stripe's fields in its renderCardFields slot. It's a paid piece. The mask hook guide covers phone and date fields for free, and our post on credit card input formatting shows the free route for cards.
In our October 2026 test, react-input-mask 2.0.4 and react-toast-notifications 2.5.1. react-s-alert, react-notification-system, react-vis, react-stripe-elements and react-sticky also failed on React 19, from other removed APIs.