Skip to content

React ref Type Mismatch: MutableRefObject and LegacyRef

Problem Statement

When working with React refs and TypeScript, you may encounter this type compatibility error:

typescript
Type 'MutableRefObject<HTMLInputElement | undefined>' is not assignable to type 'LegacyRef<HTMLInputElement> | undefined'.
  Type 'MutableRefObject<HTMLInputElement | undefined>' is not assignable to type 'RefObject<HTMLInputElement>'.
    Types of property 'current' are incompatible.
      Type 'HTMLInputElement | undefined' is not assignable to type 'HTMLInputElement | null'.
        Type 'undefined' is not assignable to type 'HTMLInputElement | null'.ts(2322)

This commonly occurs when:

  1. Using React.useRef() without passing an initial value
  2. Trying to connect the ref to a React component/DOM element (like <input>)
  3. Having TypeScript's strict type checking enabled

The core issue is a type mismatch between what React expects for ref props versus how your ref was defined. React expects a ref object that allows null (for unmounted elements), while useRef() without initialization creates a ref with potential undefined values.

Here's the problematic pattern:

tsx
const InputComponent = React.forwardRef((props: any, ref) => {
    // ❌ Problem: No initial value + undefined in type
    const handleRef = React.useRef<HTMLInputElement | undefined>()
    
    React.useImperativeHandle(ref, () => ({
        setChecked(checked: boolean) {
            if (handleRef.current) { // Current could be undefined
                handleRef.current.checked = checked;
            }
        }
    }), []);
    
    // Triggers type error:
    return <input ref={handleRef} type="checkbox" />
});

Solution: Initialize With null and Adjust Types

Fixing the Ref Initialization

The correct approach is to initialize the ref with null and adjust the type to include null instead of undefined:

tsx
const InputComponent = React.forwardRef((props, ref) => {
    // ✅ Solution: Initialize with null + correct type
    const handleRef = React.useRef<HTMLInputElement | null>(null)
    
    React.useImperativeHandle(ref, () => ({
        setChecked(checked: boolean) {
            // Still need null check for safety
            if (handleRef.current) {
                handleRef.current.checked = checked;
            }
        }
    }), []);
    
    return <input ref={handleRef} type="checkbox" />
});

Why This Works

Key Concepts

  • Uninitialized refs: useRef<T>() creates a ref with current: undefined
  • React's ref expectations: DOM elements require current: HTMLElement | null
  • null vs undefined: React uses null to represent unmounted elements

By initializing with null, you align your ref's type with React's requirements:

typescript
// What React expects:
type LegacyRef<T> = RefObject<T> | MutableRefObject<T> | null

// What your ref was originally:
handleRef.current: HTMLInputElement | undefined

// What it becomes after fix:
handleRef.current: HTMLInputElement | null

The same solution applies to similar scenarios like custom hooks:

[Fixing useOnClickOutside Hook]
tsx
const notificationsTrayRef = useRef<HTMLElement | null>(null) // ✅

// Custom hook definition
export function useOnClickOutside(
    ref: React.MutableRefObject<HTMLElement | null>, // ✅ Accept null
    handler: () => void
): void { ... }

Best Practices for Refs with TypeScript

Ref Initialization Patterns

PatternExampleWhen to Use
DOM RefsuseRef<ElementType|null>(null)Connecting to JSX elements
Mutable ValuesuseRef<ValueType>()Storing non-rendering values
Imperative HandlesuseRef<ComponentType|null>(null)Accessing child component methods

Safe Ref Access

Important

Even after the type fix, always validate refs before access:

tsx
if (handleRef.current) {
    // Safely use handleRef.current properties
}

Functional Update Caveats

Avoid this pattern with refs in effects:

tsx
// ❌ Dependency warning + stale closure risk
useEffect(() => {
    if (handleRef.current) {
        // Do something
    }
}, [handleRef.current]) // ❌ Ref.current as dependency

// ✅ Better solution
useEffect(() => {
    const current = handleRef.current
    if (current) {
        // Use current inside closure
    }
}, [/* No dependency required */])

Type Compatibility Explained

Understanding the ref type hierarchy helps prevent these errors:

Common Problem Scenarios

  1. Custom hooks with ref parameters:

    ts
    // ❌ Problem:
    function useCustom(ref: MutableRefObject<Element|undefined>)
    
    // ✅ Solution:
    function useCustom(ref: RefObject<Element|null> | MutableRefObject<Element|null>)
  2. Forwarding non-DOM refs:

    tsx
    // ✅ Proper typing for forwarded refs:
    const Component = React.forwardRef<HTMLInputElement, PropsType>(
      (props, ref) => { ... }
    )
  3. Class component refs:

    tsx
    // For class components, you want InstanceType
    const ref = useRef<MyClassComponent | null>(null)

This solution ensures your ref typing aligns with React's expectations while maintaining type safety throughout your TypeScript application.