Fix: React forwardRef Not Working — ref is null or Component Not Exposing Methods
Quick Answer
How to fix React forwardRef issues — ref null on custom components, useImperativeHandle setup, forwardRef with TypeScript, class components, and React 19 ref as prop changes.
The Problem
A ref attached to a custom component is null:
const inputRef = useRef(null);
// After render: inputRef.current is null
return <CustomInput ref={inputRef} />;
// CustomInput doesn't forward the ref
function CustomInput({ value, onChange }) {
return <input value={value} onChange={onChange} />;
}Or useImperativeHandle doesn’t expose the expected methods:
// Parent trying to call focus()
inputRef.current.focus(); // TypeError: Cannot read properties of null
// Child uses useImperativeHandle but parent's ref is still nullOr TypeScript complains about ref types:
// Error: Type 'MutableRefObject<null>' is not assignable to type
// 'Ref<HTMLInputElement> | undefined'Or in React 19, the old forwardRef API shows deprecation warnings.
Why This Happens
React doesn’t forward refs through components automatically. By default, a ref on a custom component (<MyInput ref={ref} />) attaches to nothing — the component must explicitly forward it.
Key reasons refs come back as null:
- Component not wrapped in
forwardRef()— the most common cause. React ignoresrefon custom components unless they useforwardRef(React 18 and earlier) or acceptrefas a prop (React 19+). useImperativeHandlewithoutforwardRef—useImperativeHandleonly works inside aforwardRef-wrapped component. Using it in a regular component does nothing.- Ref attached before mount —
ref.currentisnullduring the first render before the component mounts. Access the ref inuseEffector event handlers, not during render. - Conditional rendering removing the element — if the element the ref points to is conditionally unmounted (
{show && <input ref={ref} />}), the ref becomesnullwhenshowis false. - Wrong element targeted —
forwardRefreceives therefargument but passes it to the wrong element or doesn’t pass it at all.
Fix 1: Wrap Components with forwardRef
The standard fix for React 18 and earlier:
import { forwardRef, useRef } from 'react';
// WRONG — ref not forwarded
function CustomInput({ value, onChange, placeholder }) {
return (
<input
value={value}
onChange={onChange}
placeholder={placeholder}
// ref is not forwarded — parent's ref is null
/>
);
}
// CORRECT — forwardRef passes the ref to the inner element
const CustomInput = forwardRef(function CustomInput(
{ value, onChange, placeholder },
ref // Second argument — the forwarded ref
) {
return (
<input
ref={ref} // Attach the ref to the actual DOM element
value={value}
onChange={onChange}
placeholder={placeholder}
/>
);
});
// Or with arrow function syntax
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
// Usage — ref.current is now the <input> DOM element
function Parent() {
const inputRef = useRef(null);
return (
<>
<CustomInput ref={inputRef} value="" onChange={() => {}} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
}Forwarding ref through multiple layers:
// Outer wrapper — forwards ref down
const FormField = forwardRef((props, ref) => (
<div className="form-field">
<label>{props.label}</label>
<CustomInput ref={ref} {...props} /> {/* Passes ref to CustomInput */}
</div>
));
// CustomInput — forwards ref to the DOM input
const CustomInput = forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
// Parent accesses the inner <input> DOM element
const inputRef = useRef(null);
<FormField ref={inputRef} label="Name" />;
// inputRef.current is the <input> elementFix 2: Expose Custom Methods with useImperativeHandle
When you want to expose specific methods instead of the raw DOM node:
import { forwardRef, useRef, useImperativeHandle } from 'react';
// WRONG — exposes the raw DOM node (all DOM APIs exposed)
const PasswordInput = forwardRef((props, ref) => {
return <input type="password" ref={ref} {...props} />;
// Parent can call ref.current.value, ref.current.style, etc. — too much access
});
// CORRECT — expose only the methods the parent should use
const PasswordInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only these methods are accessible from the parent
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
props.onChange?.({ target: { value: '' } });
}
},
getValue: () => inputRef.current?.value ?? '',
}));
return <input type="password" ref={inputRef} {...props} />;
});
// Parent usage
function LoginForm() {
const passwordRef = useRef(null);
function handleError() {
passwordRef.current?.clear(); // Only calls clear() — can't access DOM directly
passwordRef.current?.focus();
}
return (
<form>
<PasswordInput ref={passwordRef} onChange={handleChange} />
<button type="submit">Login</button>
</form>
);
}useImperativeHandle with dependencies:
const VideoPlayer = forwardRef(({ src, autoplay }, ref) => {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
seek: (time) => {
if (videoRef.current) videoRef.current.currentTime = time;
},
getCurrentTime: () => videoRef.current?.currentTime ?? 0,
}), []); // Empty deps — methods don't change. Add deps if methods use changing values.
return <video ref={videoRef} src={src} autoPlay={autoplay} />;
});Fix 3: TypeScript Typing for forwardRef
TypeScript requires explicit type parameters for forwardRef:
import { forwardRef, useRef, useImperativeHandle } from 'react';
// Type the ref element (what ref.current will be)
// Type the component props
const CustomInput = forwardRef<HTMLInputElement, InputProps>(
function CustomInput({ label, ...props }, ref) {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
);
}
);
// Usage with correct types
const ref = useRef<HTMLInputElement>(null);
<CustomInput ref={ref} label="Name" />;
ref.current?.focus(); // TypeScript knows this is HTMLInputElementTypeScript with useImperativeHandle — define the exposed interface:
// Define what the parent can call
interface VideoPlayerHandle {
play: () => void;
pause: () => void;
seek: (time: number) => void;
getCurrentTime: () => number;
}
interface VideoPlayerProps {
src: string;
autoplay?: boolean;
}
// Type the ref as the custom handle, not HTMLVideoElement
const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
function VideoPlayer({ src, autoplay }, ref) {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play: () => { videoRef.current?.play(); },
pause: () => { videoRef.current?.pause(); },
seek: (time) => {
if (videoRef.current) videoRef.current.currentTime = time;
},
getCurrentTime: () => videoRef.current?.currentTime ?? 0,
}));
return <video ref={videoRef} src={src} autoPlay={autoplay} />;
}
);
// Parent
const playerRef = useRef<VideoPlayerHandle>(null);
<VideoPlayer ref={playerRef} src="/video.mp4" />;
playerRef.current?.play(); // TypeScript: OK
playerRef.current?.currentTime; // TypeScript Error — not in VideoPlayerHandleFix 4: React 19 — ref as a Prop
React 19 removes the need for forwardRef — ref is now a regular prop:
// React 19+ — no forwardRef needed
function CustomInput({ ref, value, onChange }) {
return <input ref={ref} value={value} onChange={onChange} />;
}
// Usage is unchanged
const inputRef = useRef(null);
<CustomInput ref={inputRef} value="" onChange={() => {}} />;TypeScript in React 19:
import { Ref } from 'react';
interface CustomInputProps {
ref?: Ref<HTMLInputElement>; // ref is a regular prop
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
function CustomInput({ ref, value, onChange }: CustomInputProps) {
return <input ref={ref} value={value} onChange={onChange} />;
}Migration strategy — make forwardRef work in both React 18 and 19:
// Wrapper that works in both versions
// In React 18: forwardRef is required
// In React 19: forwardRef is a no-op wrapper
const CustomInput = forwardRef<HTMLInputElement, InputProps>(
({ label, ...props }, ref) => (
<input ref={ref} aria-label={label} {...props} />
)
);
// React 19 shows a deprecation warning for forwardRef
// Migrate when dropping React 18 supportFix 5: Callback Refs
An alternative to useRef — a callback function that fires when the element mounts/unmounts:
function MeasuredComponent() {
const [height, setHeight] = useState(0);
// Callback ref — called with the element when it mounts
// Called with null when it unmounts
const measuredRef = useCallback((node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []); // Empty deps — function identity is stable
return (
<>
<div ref={measuredRef}>
Content to measure
</div>
<p>Height: {height}px</p>
</>
);
}Forward a callback ref through components:
const CustomInput = forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
// Callback ref works the same as useRef with forwardRef
<CustomInput
ref={(el) => {
console.log('Input mounted:', el);
// el is the HTMLInputElement — or null when unmounted
}}
/>Fix 6: Common forwardRef Mistakes
Forgetting to pass the ref to the DOM element:
// WRONG — ref is received but not passed to any element
const BadInput = forwardRef(({ value }, ref) => {
// ref is not passed to <input> — inputRef.current is still null
return <input value={value} onChange={() => {}} />;
});
// CORRECT
const GoodInput = forwardRef(({ value, onChange }, ref) => {
return <input ref={ref} value={value} onChange={onChange} />;
});Using ref.current before mount:
function Parent() {
const ref = useRef(null);
// WRONG — ref.current is null during render
console.log(ref.current?.value); // null
// CORRECT — access ref in useEffect (after mount) or event handlers
useEffect(() => {
console.log(ref.current?.value); // Has value after mount
ref.current?.focus();
}, []);
return <CustomInput ref={ref} />;
}Misusing ref in class components:
// Class components — use createRef or callback ref
class ClassInput extends React.Component {
inputRef = React.createRef();
focus() {
this.inputRef.current?.focus();
}
render() {
return <input ref={this.inputRef} />;
}
}
// To attach a ref to a class component from a parent:
const classRef = useRef(null);
<ClassInput ref={classRef} />;
// classRef.current is the ClassInput INSTANCE
// classRef.current.focus() calls the class methodFix 7: Debug ref Issues
When a ref is unexpectedly null:
// Add logging to the ref callback to diagnose mount/unmount timing
<CustomInput
ref={(el) => {
console.log('ref callback fired:', el);
// null = unmounted, element = mounted
inputRef.current = el;
}}
/>
// Check if forwardRef is applied
console.log(CustomInput.$$typeof); // Symbol(react.forward_ref) if using forwardRef
// Verify the component is mounted before accessing the ref
useEffect(() => {
console.log('After mount, ref:', inputRef.current);
}, []);
// If ref is null after mount, check:
// 1. Is the component wrapped in forwardRef?
// 2. Is the ref prop passed to the DOM element inside the component?
// 3. Is the element conditionally rendered?Still Not Working?
HOC (Higher Order Components) losing refs — if a component is wrapped in an HOC (like connect from Redux or withRouter), refs point to the HOC wrapper, not the inner component. Either forward refs through the HOC or use useImperativeHandle on the HOC.
React.memo and refs — React.memo wraps a component but doesn’t automatically forward refs. Wrap the inner component with forwardRef first, then apply memo:
const MemoizedInput = React.memo(
forwardRef((props, ref) => <input ref={ref} {...props} />)
);StrictMode double-invocation — in React StrictMode, refs are attached and detached twice in development to detect side effects. Don’t trigger critical side effects in ref callbacks that only run once.
Dynamic ref.current access in loops — avoid accessing ref.current inside a useEffect dependency array value. The ref itself is stable, but its .current value changes.
For related React issues, see Fix: React Portal Event Bubbling and Fix: React Suspense Not Triggering.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: cmdk Not Working — Command Palette Not Opening, Items Not Filtering, or Keyboard Navigation Broken
How to fix cmdk command palette issues — Dialog setup, custom filtering, groups and separators, keyboard shortcuts, async search, nested pages, and integration with shadcn/ui and Tailwind.
Fix: Conform Not Working — Form Validation Not Triggering, Server Errors Missing, or Zod Schema Rejected
How to fix Conform form validation issues — useForm setup with Zod, server action integration, nested and array fields, file uploads, progressive enhancement, and Remix and Next.js usage.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Lingui Not Working — Messages Not Extracted, Translations Missing, or Macro Errors
How to fix Lingui.js i18n issues — setup with React, message extraction, macro compilation, ICU format, lazy loading catalogs, and Next.js integration.