Skip to content

解决 'Cannot assign to current because it is a read-only property' 错误

问题描述

在 TypeScript 和 React 开发中,当使用 useRef 钩子创建引用对象时,有时会遇到以下错误:

Cannot assign to 'current' because it is a read-only property

这个错误通常在尝试修改 useRef.current 属性时发生。从问题中的代码来看,核心问题是:

typescript
const unblockRef = useRef<() => void | null>(null);

// 在 useEffect 中修改时出错
unblockRef.current = null;

尽管相似的 string | null 类型(如 testRef)可以正常工作,但当尝试将 ref 对象声明为函数类型或 null 时,TypeScript 会认为 current 属性是只读的,从而阻止赋值操作。

错误根源

错误的原因在于 TypeScript 中的操作符优先级useRef 的返回类型机制:

  1. 类型解析问题
    类型定义 () => void | null 会被 TypeScript 解释为:

    • 一个返回 void | null 类型的函数(即函数返回值可以是 voidnull
    • 而不是期望的 "函数类型或 null"

    这是因为 | 操作符具有更高优先级,等同于:

    typescript
    () => (void | null)
  2. React 引用类型机制
    React 会根据泛型类型决定返回 RefObjectMutableRefObject

    • 如果泛型类型包含 null(如 T | null),返回 MutableRefObject
    • 反之返回只读的 RefObject(current 不可修改)

解决方案

方案 1:修正类型定义(推荐)

使用括号明确声明函数类型,确保操作符优先级正确:

typescript
// 用括号包裹函数类型
const unblockRef = useRef<(() => void) | null>(null);

为什么这样可以解决问题?

  • (() => void) | null 明确表示 "要么是函数类型,要么是 null"
  • 包含 null 的联合类型使 React 返回可变的 MutableRefObject

方案 2:显式声明 MutableRefObject

如果需要更明确的类型声明,可直接指定引用类型:

typescript
import { MutableRefObject, useRef } from 'react';

const unblockRef: MutableRefObject<(() => void) | null> = useRef(null);
ts
// 简洁写法
const ref = useRef<(() => void) | null>(null);
ts
// 显式声明可变引用
import { MutableRefObject } from 'react';

const ref: MutableRefObject<(() => void) | null> = useRef(null);

理解 RefObject 与 MutableRefObject

理解这两者的区别对解决问题至关重要:

特性RefObjectMutableRefObject
current 属性只读 (readonly)可变 (mutable)
使用场景DOM 引用可变值
触发条件泛型包含 null泛型包含 null
TS 错误示例ref.current = value-
正确声明示例useRef<HTMLDivElement>(null)useRef<string|null>(null)

重要设计原则

  1. 如果需要修改 current 属性,泛型必须包含 null(如 T | null
  2. 对于函数类型,必须用括号明确语法:(函数类型) | null

实际应用示例

以下是包含完整修正的代码实现:

tsx
import { useState, useRef, useEffect } from "react";

export default function App() {
  // 字符串引用 - 正确声明
  const testRef = useRef<string | null>(null);
  
  // 函数引用 - 修正后
  const unblockRef = useRef<(() => void) | null>(null);

  useEffect(() => {
    const test = () => console.log("hey");
    
    testRef.current = "value"; // 正常赋值
    unblockRef.current = test; // 修正后不再报错
    
    // 设为 null 也正常
    unblockRef.current = null; 
  }, []);

  return (
    <div className="App">
      {/* 组件内容 */}
    </div>
  );
}

总结

当遇到 "Cannot assign to 'current' because it is a read-only property" 错误时:

  1. 检查类型定义:对函数类型使用括号 (函数类型) | null
  2. 确认包含 null:确保联合类型包含 null 以启用可变引用
  3. 理解引用类型:React 根据泛型返回 RefObject(只读)或 MutableRefObject(可变)

按正确的语法修正类型定义后,即可自由修改 current 属性。这种模式在管理回调函数引用、存储可变值等场景中非常实用。

最佳实践

  • 优先使用方案一的简洁语法 useRef<(() => void) | null>(null)
  • 仅在复杂场景下使用显式的 MutableRefObject 声明
  • current 不需修改时,使用不包含 null 的类型以保持只读特性

通过理解 TypeScript 的类型机制和 React 的引用设计原理,可以彻底解决此错误并编写出更健壮的代码。