前言
在 React 开发中,引用(Ref)是一个重要的概念,它允许我们直接访问 DOM 元素或组件实例。对于初学者来说,理解 React 的引用系统及其相关类型定义是掌握高级 React 开发的关键。本文将详细介绍 React 中所有与引用相关的类型,并提供实用的代码示例。
什么是 React 引用?
在 React 中,数据通常通过 props 向下流动,这被称为"单向数据流"。但有时我们需要直接访问 DOM 元素或组件实例,这时就需要使用引用(Ref)。
React 对象 vs 原生 DOM
首先要理解一个重要概念:JSX 生成的是 React 对象,不是原生 DOM 对象。React 对象通过渲染过程转换为真实 DOM,不能直接调用 DOM 方法,需要通过 ref 获取真实 DOM。
javascript
// 这是一个 React 对象
const element = <div>Hello World</div>;
console.log(element);
// 输出: { type: 'div', props: { children: 'Hello World' }, key: null }
// 要访问真实 DOM,需要使用 ref
const MyComponent = () => {
const divRef = useRef(null);
useEffect(() => {
console.log(divRef.current); // 这才是真实的 DOM 元素
divRef.current.focus(); // 可以调用 DOM 方法
}, []);
return <div ref={divRef}>Hello World</div>;
};
React 引用相关类型详解
1. React.Ref
这是最基础的引用类型,它是一个联合类型:
typescript
type Ref<T> =
| ((instance: T | null) => void) // 回调 ref
| RefObject<T> // useRef 创建的对象
| null;
使用场景:
- 回调 ref:当你需要在元素挂载/卸载时执行逻辑
- RefObject:使用 useRef Hook 创建的引用
- null:不需要引用时
typescript
// 回调 ref 示例
const CallbackRefExample = () => {
const callbackRef = (element: HTMLDivElement | null) => {
if (element) {
console.log('元素已挂载:', element);
element.style.backgroundColor = 'lightblue';
} else {
console.log('元素已卸载');
}
};
return <div ref={callbackRef}>使用回调 ref</div>;
};
// RefObject 示例
const RefObjectExample = () => {
const divRef = useRef<HTMLDivElement>(null);
const handleClick = () => {
if (divRef.current) {
divRef.current.scrollIntoView();
}
};
return (
<div>
<button onClick={handleClick}>滚动到目标元素</button>
<div ref={divRef}>目标元素</div>
</div>
);
};
2. React.RefObject
这是 useRef
Hook 返回的对象类型:
typescript
interface RefObject<T> {
readonly current: T | null;
}
特点:
current
属性是只读的- 初始值可以是
null
- 主要用于访问 DOM 元素
typescript
const InputExample = () => {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
// TypeScript 会确保类型安全
inputRef.current?.focus();
};
const getValue = () => {
// 安全地访问 value 属性
const value = inputRef.current?.value || '';
console.log('输入值:', value);
};
return (
<div>
<input ref={inputRef} placeholder="点击按钮聚焦" />
<button onClick={focusInput}>聚焦输入框</button>
<button onClick={getValue}>获取值</button>
</div>
);
};
3. React.MutableRefObject
这是可变引用对象的类型:
typescript
interface MutableRefObject<T> {
current: T;
}
特点:
current
属性可以修改- 不会是
null
(除非你明确设置) - 常用于存储不需要触发重新渲染的值
typescript
const CounterExample = () => {
const countRef = useRef<number>(0);
const [, forceUpdate] = useState({});
const increment = () => {
countRef.current += 1;
console.log('当前计数:', countRef.current);
// 注意:修改 ref 不会触发重新渲染
};
const forceRender = () => {
forceUpdate({}); // 强制重新渲染以显示最新值
};
return (
<div>
<p>计数: {countRef.current}</p>
<button onClick={increment}>增加</button>
<button onClick={forceRender}>强制更新显示</button>
</div>
);
};
4. React.RefAttributes
这个类型包含 ref 属性的定义:
typescript
interface RefAttributes<T = any> {
ref?: Ref<T> | undefined;
}
用途:
- 在组件 props 类型中包含 ref 属性
- 与其他 props 类型组合使用
typescript
// 自定义组件的 props 类型
interface CustomButtonProps extends RefAttributes<HTMLButtonElement> {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
onClick?: () => void;
}
const CustomButton = React.forwardRef<HTMLButtonElement, Omit<CustomButtonProps, 'ref'>>(
({ children, variant = 'primary', onClick }, ref) => {
return (
<button
ref={ref}
onClick={onClick}
className={`btn btn-${variant}`}
>
{children}
</button>
);
}
);
5. React.ForwardRefExoticComponent
这是使用 React.forwardRef
创建的组件的类型:
typescript
declare const Search: React.ForwardRefExoticComponent<SearchProps & React.RefAttributes<InputRef>>;
特点:
- 支持 ref 转发
- 可以让父组件直接访问子组件的 DOM 元素
- 常用于组件库开发
typescript
// 创建一个支持 ref 转发的输入组件
interface SearchProps {
placeholder?: string;
onSearch?: (value: string) => void;
disabled?: boolean;
}
const SearchInput = React.forwardRef<HTMLInputElement, SearchProps>(
({ placeholder, onSearch, disabled }, ref) => {
const [value, setValue] = useState('');
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && onSearch) {
onSearch(value);
}
};
return (
<input
ref={ref}
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder={placeholder}
disabled={disabled}
/>
);
}
);
// 使用示例
const SearchExample = () => {
const searchRef = useRef<HTMLInputElement>(null);
const focusSearch = () => {
searchRef.current?.focus();
};
const handleSearch = (value: string) => {
console.log('搜索:', value);
};
return (
<div>
<SearchInput
ref={searchRef}
placeholder="输入搜索内容"
onSearch={handleSearch}
/>
<button onClick={focusSearch}>聚焦搜索框</button>
</div>
);
};
useRef Hook 的不同用法
1. 访问 DOM 元素
typescript
const DOMAccessExample = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const playVideo = () => {
videoRef.current?.play();
};
const pauseVideo = () => {
videoRef.current?.pause();
};
return (
<div>
<video ref={videoRef} width="300" height="200">
<source src="video.mp4" type="video/mp4" />
</video>
<div>
<button onClick={playVideo}>播放</button>
<button onClick={pauseVideo}>暂停</button>
</div>
</div>
);
};
2. 存储可变值
typescript
const TimerExample = () => {
const [count, setCount] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const startTimer = () => {
if (intervalRef.current) return; // 防止重复启动
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useEffect(() => {
// 组件卸载时清理定时器
return () => stopTimer();
}, []);
return (
<div>
<p>计时器: {count}秒</p>
<button onClick={startTimer}>开始</button>
<button onClick={stopTimer}>停止</button>
</div>
);
};
3. 保存前一个值
typescript
const usePrevious = <T>(value: T): T | undefined => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const PreviousValueExample = () => {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>当前值: {count}</p>
<p>前一个值: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
};
高级用法:Ref 转发
高阶组件中的 Ref 转发
typescript
// 高阶组件,添加日志功能
function withLogging<P extends object>(
Component: React.ComponentType<P>
) {
const WithLoggingComponent = React.forwardRef<any, P>((props, ref) => {
useEffect(() => {
console.log('组件已挂载:', Component.name);
return () => console.log('组件将卸载:', Component.name);
}, []);
return <Component {...props} ref={ref} />;
});
WithLoggingComponent.displayName = `withLogging(${Component.displayName || Component.name})`;
return WithLoggingComponent;
}
// 使用示例
const Button = React.forwardRef<HTMLButtonElement, { children: React.ReactNode }>(
({ children }, ref) => {
return <button ref={ref}>{children}</button>;
}
);
const LoggedButton = withLogging(Button);
条件 Ref 转发
typescript
interface ConditionalRefProps {
children: React.ReactNode;
enableRef?: boolean;
}
const ConditionalRef = React.forwardRef<HTMLDivElement, ConditionalRefProps>(
({ children, enableRef = true }, ref) => {
return (
<div ref={enableRef ? ref : null}>
{children}
</div>
);
}
);
类型安全的最佳实践
1. 使用 TypeScript 索引访问类型
根据项目的 React 事件处理规范,我们可以使用 TypeScript 索引访问类型来提取事件处理器类型:
typescript
// 从组件 props 中提取特定属性的类型
import { Input } from 'antd';
type SearchProps = React.ComponentProps<typeof Input.Search>;
type OnSearchType = SearchProps['onSearch']; // 提取 onSearch 的类型
const SearchComponent = () => {
// 使用提取的类型确保类型安全
const handleSearch: OnSearchType = (value, event) => {
console.log('搜索值:', value);
console.log('事件对象:', event);
};
return (
<Input.Search
placeholder="请输入搜索内容"
onSearch={handleSearch}
enterButton="搜索"
/>
);
};
2. 事件处理函数的类型定义
typescript
// 键盘事件处理
const handleKeyPress: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
if (e.key === 'Enter') {
// 处理回车事件
console.log('按下回车键');
}
};
// 鼠标事件处理
const handleMouseClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
console.log('按钮被点击');
};
// 表单事件处理
const handleFormSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
console.log('表单提交');
};
3. 条件事件处理
typescript
const ConditionalEventExample = () => {
const [isEnabled, setIsEnabled] = useState(true);
const handleClick = isEnabled
? () => console.log('点击处理')
: undefined;
return (
<div>
<button onClick={handleClick} disabled={!isEnabled}>
{isEnabled ? '启用的按钮' : '禁用的按钮'}
</button>
<button onClick={() => setIsEnabled(!isEnabled)}>
切换状态
</button>
</div>
);
};
常见错误和解决方案
1. Ref 在组件挂载前访问
typescript
// ❌ 错误:在组件挂载前访问 ref
const BadExample = () => {
const inputRef = useRef<HTMLInputElement>(null);
// 这里会报错,因为组件还没有挂载
inputRef.current?.focus(); // TypeError: Cannot read property 'focus' of null
return <input ref={inputRef} />;
};
// ✅ 正确:在 useEffect 中访问 ref
const GoodExample = () => {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// 组件挂载后安全访问
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
};
2. 函数组件中的 Ref 转发
typescript
// ❌ 错误:函数组件不能直接接收 ref
const BadComponent = ({ ref }: { ref: React.Ref<HTMLDivElement> }) => {
return <div ref={ref}>内容</div>;
};
// ✅ 正确:使用 forwardRef
const GoodComponent = React.forwardRef<HTMLDivElement, {}>((props, ref) => {
return <div ref={ref}>内容</div>;
});
3. Ref 类型不匹配
typescript
// ❌ 错误:类型不匹配
const BadTypeExample = () => {
const ref = useRef<HTMLInputElement>(null);
return <div ref={ref}>这里应该是 div,不是 input</div>; // 类型错误
};
// ✅ 正确:类型匹配
const GoodTypeExample = () => {
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
return (
<div ref={divRef}>
<input ref={inputRef} />
</div>
);
};
实际项目中的应用场景
1. 表单焦点管理
typescript
const FormExample = () => {
const nameRef = useRef<HTMLInputElement>(null);
const emailRef = useRef<HTMLInputElement>(null);
const submitRef = useRef<HTMLButtonElement>(null);
const handleNameEnter = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
emailRef.current?.focus();
}
};
const handleEmailEnter = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
submitRef.current?.focus();
}
};
return (
<form>
<input
ref={nameRef}
placeholder="姓名"
onKeyPress={handleNameEnter}
/>
<input
ref={emailRef}
type="email"
placeholder="邮箱"
onKeyPress={handleEmailEnter}
/>
<button ref={submitRef} type="submit">
提交
</button>
</form>
);
};
2. 滚动控制
typescript
const ScrollExample = () => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const scrollToTop = () => {
scrollContainerRef.current?.scrollTo({
top: 0,
behavior: 'smooth'
});
};
const scrollToBottom = () => {
const container = scrollContainerRef.current;
if (container) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
}
};
return (
<div>
<div>
<button onClick={scrollToTop}>滚动到顶部</button>
<button onClick={scrollToBottom}>滚动到底部</button>
</div>
<div
ref={scrollContainerRef}
style={{ height: '200px', overflow: 'auto' }}
>
{Array.from({ length: 50 }, (_, i) => (
<div key={i} style={{ padding: '10px' }}>
内容行 {i + 1}
</div>
))}
</div>
</div>
);
};
3. 动画控制
typescript
const AnimationExample = () => {
const boxRef = useRef<HTMLDivElement>(null);
const startAnimation = () => {
const box = boxRef.current;
if (box) {
box.style.transition = 'transform 0.5s ease-in-out';
box.style.transform = 'translateX(100px) rotate(45deg)';
}
};
const resetAnimation = () => {
const box = boxRef.current;
if (box) {
box.style.transform = 'translateX(0) rotate(0deg)';
}
};
return (
<div>
<div
ref={boxRef}
style={{
width: '50px',
height: '50px',
backgroundColor: 'blue',
margin: '20px'
}}
/>
<button onClick={startAnimation}>开始动画</button>
<button onClick={resetAnimation}>重置</button>
</div>
);
};
性能优化技巧
1. 避免不必要的 Ref 创建
typescript
// ❌ 不好:每次渲染都创建新的 ref
const BadPerformance = () => {
return (
<div>
{[1, 2, 3].map(item => (
<input key={item} ref={useRef<HTMLInputElement>(null)} />
))}
</div>
);
};
// ✅ 更好:使用回调 ref 或 useMemo
const GoodPerformance = () => {
const inputRefs = useMemo(() =>
Array.from({ length: 3 }, () => createRef<HTMLInputElement>()),
[]
);
return (
<div>
{[1, 2, 3].map((item, index) => (
<input key={item} ref={inputRefs[index]} />
))}
</div>
);
};
2. 延迟 Ref 操作
typescript
const LazyRefExample = () => {
const expensiveRef = useRef<HTMLCanvasElement>(null);
const initializeCanvas = useCallback(() => {
const canvas = expensiveRef.current;
if (canvas) {
const ctx = canvas.getContext('2d');
// 执行复杂的画布初始化
ctx?.fillRect(0, 0, canvas.width, canvas.height);
}
}, []);
// 延迟到用户交互时再初始化
const handleUserInteraction = () => {
initializeCanvas();
};
return (
<div>
<canvas ref={expensiveRef} width={800} height={600} />
<button onClick={handleUserInteraction}>初始化画布</button>
</div>
);
};
总结
React 的引用系统提供了强大而灵活的方式来直接操作 DOM 元素和组件实例。理解这些类型定义和最佳实践对于编写高质量的 React 应用至关重要:
关键要点
- React 对象 vs DOM 对象:JSX 创建的是 React 对象,需要通过 ref 访问真实 DOM
- 类型安全:使用 TypeScript 确保 ref 类型的正确性
- Ref 转发 :使用
forwardRef
让组件支持 ref 传递 - 事件处理:遵循项目的事件处理规范,使用索引访问类型提取事件处理器类型
- 性能考虑:避免不必要的 ref 创建和操作
最佳实践
- 优先使用 React 的声明式方式,只在必要时使用 ref
- 在
useEffect
中安全地访问 ref - 使用 TypeScript 确保类型安全
- 遵循项目的代码规范和事件处理规范
- 合理使用 ref 转发提高组件的可复用性