Skip to content

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:

typescript
// 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:

typescript
// Corrected declaration using parentheses
const unblockRef = useRef<(() => void) | null>(null);

Parentheses ensure correct type interpretation:

  1. (() => void) | null = either a void-returning function OR null
  2. Without parentheses, () => void | null = function returning void OR null

Understanding MutableRefObject vs. RefObject

React differentiates between two ref types:

  1. MutableRefObject: Allows reassigning .current
  2. RefObject: Makes .current read-only

When you include | null in your initial type, TypeScript automatically infers a MutableRefObject:

ts
// .current is mutable
const mutableRef = useRef<string | null>(null);
mutableRef.current = "new value"; // ✅ Works
ts
// .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:

typescript
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

typescript
// ❌ 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:

typescript
useEffect(() => {
  // ❌ Would error with problematic ref
  // unblockRef.current = null;
  
  // ✅ Works with corrected ref
  workingRef.current = () => console.log("Fixed!");
}, []);

Common Pitfalls and Solutions

  1. Functions in Union Types: Always wrap function types in parentheses

    ts
    // ✅ Correct syntax
    const fnRef = useRef<((arg: string) => number) | null>(null);
  2. Initialized vs Uninitialized Refs:

    ts
    // 🔒 .current is read-only
    const ref1 = useRef<HTMLElement>();
    // 🔓 .current is mutable
    const ref2 = useRef<HTMLElement | null>(null);
  3. DOM Element Refs: Apply the same pattern

    tsx
    const inputRef = useRef<HTMLInputElement | null>(null);
    // ...
    <input ref={inputRef} />

Best Practices

  1. Consistent Null Initialization:

    • Initialize useRef(null) to get mutable refs
    • Always include | null in your type when mutation is needed
  2. Use Explicit Types When Necessary:

    ts
    const timerRef = useRef<number | undefined>();
  3. Avoid Ref Overuse:

    • Prefer state (useState) for data that triggers re-renders
    • Use refs for storing values that don't affect rendering

Anti-Pattern

Don't use unions with incompatible types:

ts
// ❌ Confusing type with both function and string
const badRef = useRef<(() => void) | string>(null);