解决 '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
属性时发生。从问题中的代码来看,核心问题是:
const unblockRef = useRef<() => void | null>(null);
// 在 useEffect 中修改时出错
unblockRef.current = null;
尽管相似的 string | null
类型(如 testRef
)可以正常工作,但当尝试将 ref
对象声明为函数类型或 null
时,TypeScript 会认为 current
属性是只读的,从而阻止赋值操作。
错误根源
错误的原因在于 TypeScript 中的操作符优先级和 useRef
的返回类型机制:
类型解析问题
类型定义() => void | null
会被 TypeScript 解释为:- 一个返回
void | null
类型的函数(即函数返回值可以是void
或null
) - 而不是期望的 "函数类型或
null
"
这是因为
|
操作符具有更高优先级,等同于:typescript() => (void | null)
- 一个返回
React 引用类型机制
React 会根据泛型类型决定返回RefObject
或MutableRefObject
:- 如果泛型类型包含
null
(如T | null
),返回MutableRefObject
- 反之返回只读的
RefObject
(current 不可修改)
- 如果泛型类型包含
解决方案
方案 1:修正类型定义(推荐)
使用括号明确声明函数类型,确保操作符优先级正确:
// 用括号包裹函数类型
const unblockRef = useRef<(() => void) | null>(null);
为什么这样可以解决问题?
(() => void) | null
明确表示 "要么是函数类型,要么是 null"- 包含
null
的联合类型使 React 返回可变的MutableRefObject
方案 2:显式声明 MutableRefObject
如果需要更明确的类型声明,可直接指定引用类型:
import { MutableRefObject, useRef } from 'react';
const unblockRef: MutableRefObject<(() => void) | null> = useRef(null);
// 简洁写法
const ref = useRef<(() => void) | null>(null);
// 显式声明可变引用
import { MutableRefObject } from 'react';
const ref: MutableRefObject<(() => void) | null> = useRef(null);
理解 RefObject 与 MutableRefObject
理解这两者的区别对解决问题至关重要:
特性 | RefObject | MutableRefObject |
---|---|---|
current 属性 | 只读 (readonly) | 可变 (mutable) |
使用场景 | DOM 引用 | 可变值 |
触发条件 | 泛型不包含 null | 泛型包含 null |
TS 错误示例 | ref.current = value | - |
正确声明示例 | useRef<HTMLDivElement>(null) | useRef<string|null>(null) |
重要设计原则
- 如果需要修改
current
属性,泛型必须包含null
(如T | null
) - 对于函数类型,必须用括号明确语法:
(函数类型) | null
实际应用示例
以下是包含完整修正的代码实现:
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" 错误时:
- 检查类型定义:对函数类型使用括号
(函数类型) | null
- 确认包含 null:确保联合类型包含
null
以启用可变引用 - 理解引用类型:React 根据泛型返回
RefObject
(只读)或MutableRefObject
(可变)
按正确的语法修正类型定义后,即可自由修改 current
属性。这种模式在管理回调函数引用、存储可变值等场景中非常实用。
最佳实践
- 优先使用方案一的简洁语法
useRef<(() => void) | null>(null)
- 仅在复杂场景下使用显式的
MutableRefObject
声明 - 当
current
不需修改时,使用不包含null
的类型以保持只读特性
通过理解 TypeScript 的类型机制和 React 的引用设计原理,可以彻底解决此错误并编写出更健壮的代码。