Skip to content

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

FixDevs · (Updated: )

Part of:  React & Frontend Errors

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. This is a deliberate design decision: refs break encapsulation by giving the parent direct access to a child’s DOM node, so React requires the child to opt in.

The most common cause of a null ref is a component that accepts props but was never wrapped in forwardRef(). React silently drops the ref argument — no warning, no error. The parent creates a ref, passes it, and gets null back. This becomes dangerous when the parent calls a method on the ref (ref.current.focus()) without a null check, producing a TypeError at runtime.

Timing also matters. ref.current is always null during the first render, before the component mounts. Code that accesses the ref during render (instead of inside useEffect or an event handler) always fails. Similarly, conditionally rendered elements ({show && <input ref={ref} />}) set the ref to null when the element unmounts, and code that doesn’t account for this breaks when the element disappears.

Other causes:

  • useImperativeHandle without forwardRefuseImperativeHandle only works inside a forwardRef-wrapped component. Using it in a regular component does nothing.
  • Wrong element targetedforwardRef receives the ref argument but passes it to a wrapper <div> instead of the intended <input>.
  • HOC wrapping strips the ref — components wrapped in Higher Order Components like Redux’s connect or withRouter may lose the forwarded ref unless the HOC explicitly handles it.

In Production: Incident Lens

A null ref in production typically surfaces as a TypeError: Cannot read properties of null (reading 'focus') or Cannot read properties of null (reading 'scrollTo'). The stack trace points to a parent component that tries to imperatively call a method on a child’s ref. The error is deterministic — it happens every time the code path executes — but the blast radius is limited to the component tree that uses that ref.

How it surfaces: A Sentry or Datadog alert fires for a spike in TypeError exceptions from a specific component. Or QA reports that a modal’s auto-focus no longer works after a refactor. The refactor typically involves extracting a component into a separate file, wrapping it in a HOC, or upgrading from a class component to a function component without adding forwardRef. The error may not appear in development if the feature that triggers the ref usage (e.g., keyboard navigation, accessibility focus management) isn’t exercised during manual testing.

Blast radius: Per-component. The broken ref doesn’t crash the entire app if the error is caught by a React error boundary. Without an error boundary, the error bubbles up and unmounts the nearest parent tree. In forms, this can mean losing user input. In media players, this can mean a video/audio element that can’t be programmatically controlled.

Monitoring signals:

  • Error boundary trigger rate in the affected component tree
  • Sentry/Datadog TypeError volume filtered to “Cannot read properties of null”
  • User interaction metrics: if a “focus on mount” feature stops working, the affected input’s first-interaction time increases measurably

Recovery sequence: The fix is a code change — wrap the component in forwardRef or add a null check before calling ref methods. For immediate mitigation, deploy a null guard (ref.current?.focus()) to stop the error from crashing the component. Then add the proper forwardRef wrapper in a follow-up commit.

Postmortem preventives: Add an ESLint rule or custom lint that flags components receiving a ref prop without forwardRef. Write integration tests that assert ref-dependent behavior (e.g., “modal auto-focuses the first input on open”). Use TypeScript strictly — typed refs catch null access at compile time.

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.

Refs inside portals — a component rendered via createPortal still receives refs normally, but the DOM node lives outside the parent’s DOM tree. If your code assumes the ref target is a descendant of the parent element (e.g., for contains() checks), portal-rendered refs break that assumption. Use the portal’s container element as the boundary for hit-testing.

Server components and refs — React Server Components cannot use refs. If you mark a component as "use server" (or it’s in a server-only file), attaching a ref to it silently fails. Move ref-dependent components to client components with "use client" at the top of the file.

flushSync and ref timing — if you update state and immediately access a ref expecting the DOM to reflect the new state, the DOM may not have updated yet. Wrap the state update in flushSync to force a synchronous re-render before accessing the ref, but use this sparingly as it hurts performance.

For related React issues, see Fix: React Hydration Error, Fix: React useState Not Updating, Fix: React Hooks Called Conditionally, and Fix: React Testing Library Not Finding Element.

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