React ref Type Mismatch: MutableRefObject and LegacyRef
Problem Statement
When working with React refs and TypeScript, you may encounter this type compatibility error:
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:
- Using
React.useRef()
without passing an initial value - Trying to connect the ref to a React component/DOM element (like
<input>
) - 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:
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
:
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 withcurrent: undefined
- React's ref expectations: DOM elements require
current: HTMLElement | null
null
vsundefined
: React usesnull
to represent unmounted elements
By initializing with null
, you align your ref's type with React's requirements:
// 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]
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
Pattern | Example | When to Use |
---|---|---|
DOM Refs | useRef<ElementType|null>(null) | Connecting to JSX elements |
Mutable Values | useRef<ValueType>() | Storing non-rendering values |
Imperative Handles | useRef<ComponentType|null>(null) | Accessing child component methods |
Safe Ref Access
Important
Even after the type fix, always validate refs before access:
if (handleRef.current) {
// Safely use handleRef.current properties
}
Functional Update Caveats
Avoid this pattern with refs in effects:
// ❌ 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
Custom hooks with ref parameters:
ts// ❌ Problem: function useCustom(ref: MutableRefObject<Element|undefined>) // ✅ Solution: function useCustom(ref: RefObject<Element|null> | MutableRefObject<Element|null>)
Forwarding non-DOM refs:
tsx// ✅ Proper typing for forwarded refs: const Component = React.forwardRef<HTMLInputElement, PropsType>( (props, ref) => { ... } )
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.