React 引用(Ref)完全指南

前言

在 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 应用至关重要:

关键要点

  1. React 对象 vs DOM 对象:JSX 创建的是 React 对象,需要通过 ref 访问真实 DOM
  2. 类型安全:使用 TypeScript 确保 ref 类型的正确性
  3. Ref 转发 :使用 forwardRef 让组件支持 ref 传递
  4. 事件处理:遵循项目的事件处理规范,使用索引访问类型提取事件处理器类型
  5. 性能考虑:避免不必要的 ref 创建和操作

最佳实践

  • 优先使用 React 的声明式方式,只在必要时使用 ref
  • useEffect 中安全地访问 ref
  • 使用 TypeScript 确保类型安全
  • 遵循项目的代码规范和事件处理规范
  • 合理使用 ref 转发提高组件的可复用性
相关推荐
页面仔Dony14 分钟前
流式数据获取与展示
前端·javascript
张志鹏PHP全栈22 分钟前
postcss-px-to-viewport如何实现单页面使用?
前端
恋猫de小郭22 分钟前
iOS 26 正式版即将发布,Flutter 完成全新 devicectl + lldb 的 Debug JIT 运行支持
android·前端·flutter
前端进阶者1 小时前
electron-vite_20外部依赖包上线后如何更新
前端·javascript·electron
晴空雨1 小时前
💥 React 容器组件深度解析:从 Props 拦截到事件改写
前端·react.js·设计模式
Marshall35721 小时前
前端水印防篡改原理及实现
前端
阿虎儿1 小时前
TypeScript 内置工具类型完全指南
前端·javascript·typescript
IT_陈寒2 小时前
Java性能优化实战:5个立竿见影的技巧让你的应用提速50%
前端·人工智能·后端
chxii2 小时前
6.3Element UI 的表单
javascript·vue.js·elementui