Skip to content

Fix: React forwardRef Not Working — ref is null or Component Not Exposing Methods

FixDevs ·

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 null

Or 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 ignores ref on custom components unless they use forwardRef (React 18 and earlier) or accept ref as a prop (React 19+).
  • useImperativeHandle without forwardRefuseImperativeHandle only works inside a forwardRef-wrapped component. Using it in a regular component does nothing.
  • Ref attached before mountref.current is null during the first render before the component mounts. Access the ref in useEffect or 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 becomes null when show is false.
  • Wrong element targetedforwardRef receives the ref argument 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> element

Fix 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 HTMLInputElement

TypeScript 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 VideoPlayerHandle

Fix 4: React 19 — ref as a Prop

React 19 removes the need for forwardRefref 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 support

Fix 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 method

Fix 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 refsReact.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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles