React useRef: Cannot Assign to 'current' in TypeScript
Problem Statement
When working with React and TypeScript, you might encounter the error Cannot assign to 'current' because it is a read-only property
. This typically occurs when modifying a useRef
object that has a union type containing function types.
The core issue stems from TypeScript interpreting your type definition incorrectly:
// Problematic declaration
const unblockRef = useRef<() => void | null>(null);
Here, TypeScript interprets the type as "a function returning void | null
" rather than "either a function returning void
or null
". This makes the .current
property read-only.
Solution
To resolve this error, wrap function types in parentheses when using union types:
// Corrected declaration using parentheses
const unblockRef = useRef<(() => void) | null>(null);
Parentheses ensure correct type interpretation:
(() => void) | null
= either a void-returning function OR null- Without parentheses,
() => void | null
= function returning void OR null
Understanding MutableRefObject vs. RefObject
React differentiates between two ref types:
- MutableRefObject: Allows reassigning
.current
- RefObject: Makes
.current
read-only
When you include | null
in your initial type, TypeScript automatically infers a MutableRefObject
:
// .current is mutable
const mutableRef = useRef<string | null>(null);
mutableRef.current = "new value"; // ✅ Works
// .current is read-only
const readOnlyRef = useRef<string>();
readOnlyRef.current = "new value"; // ❌ Error!
Explicit Type Declaration (Alternative Approach)
For clarity, you can explicitly declare mutable refs:
import { MutableRefObject, useRef } from "react";
const unblockRef: MutableRefObject<(() => void) | null> = useRef(null);
Why This Error Occurs
TypeScript Operator Precedence
Union type operators (|
) have lower precedence than function type declarations. Without parentheses, TypeScript groups the return type with the union instead of treating the function as a distinct type:
Practical Examples
WARNING
Avoid this syntax
// ❌ Incorrect: Implies function returns void|null
const errorRef = useRef<() => void | null>(null);
// ✅ Correct: Clear union of function and null
const workingRef = useRef<(() => void) | null>(null);
Testing Both Types
This sandbox shows both cases:
useEffect(() => {
// ❌ Would error with problematic ref
// unblockRef.current = null;
// ✅ Works with corrected ref
workingRef.current = () => console.log("Fixed!");
}, []);
Common Pitfalls and Solutions
Functions in Union Types: Always wrap function types in parentheses
ts// ✅ Correct syntax const fnRef = useRef<((arg: string) => number) | null>(null);
Initialized vs Uninitialized Refs:
ts// 🔒 .current is read-only const ref1 = useRef<HTMLElement>(); // 🔓 .current is mutable const ref2 = useRef<HTMLElement | null>(null);
DOM Element Refs: Apply the same pattern
tsxconst inputRef = useRef<HTMLInputElement | null>(null); // ... <input ref={inputRef} />
Best Practices
Consistent Null Initialization:
- Initialize
useRef(null)
to get mutable refs - Always include
| null
in your type when mutation is needed
- Initialize
Use Explicit Types When Necessary:
tsconst timerRef = useRef<number | undefined>();
Avoid Ref Overuse:
- Prefer state (
useState
) for data that triggers re-renders - Use refs for storing values that don't affect rendering
- Prefer state (
Anti-Pattern
Don't use unions with incompatible types:
// ❌ Confusing type with both function and string
const badRef = useRef<(() => void) | string>(null);