You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React如何通过useRef定位map渲染的列表组件实现按键交互

React 动态渲染鼓垫如何匹配键盘事件触发对应交互

问题描述

我正在开发一组与键盘按键一一对应的鼓垫按钮组件,作为React初学者,我不清楚如何定位这些通过数组遍历动态渲染出的元素。目前我已为每个元素配置了唯一id,也可以正常获取键盘触发事件对应的key值,但不知道后续如何匹配到对应元素并触发交互。我曾尝试使用refs实现该功能,但没有找到能够适配我当前代码逻辑、易于理解的参考示例。

当前项目相关代码如下:

  1. 鼓垫配置状态定义
const [bankOne,setBankOne] = React.useState([
  {
    keyCode: 81,
    keyTrigger: "Q",
    id: "Heater-1",
    url: "https://s3.amazonaws.com/freecodecamp/drums/Heater-1.mp3"
  },
  {
    keyCode: 87,
    keyTrigger: "W",
    id: "Heater-2",
    url: "https://s3.amazonaws.com/freecodecamp/drums/Heater-2.mp3"
  }
]);
  1. 键盘事件绑定逻辑
const handleKeyPress = React.useCallback((event) => {
  //console.log(event.key.toUpperCase());
  console.log(itemsRef.current)
}, []);

React.useEffect(() => {
  document.addEventListener('keypress', handleKeyPress);
  return () => {
    document.removeEventListener('keypress', handleKeyPress);
  };
}, [handleKeyPress]);
  1. 动态渲染按钮列表代码
const bank1 = bankOne.map((item,i) => (
  <button
    ref={el => itemsRef.current[i] = item.keyTrigger}
    id={item.id}
    className="drum-pad"
    value={item.keyTrigger}
    onClick={handleClick}
    onKeyPress={handleKeyPress}
  >
    <audio  src={item.url} className="clip" id={item.keyTrigger}>
      <source src={item.url} type="audio/mp3" />
    </audio>
    {item.keyTrigger}
  </button>
));

此前尝试多种方案均未跑通,希望得到可落地的实现指导。


回答

你当前的ref写法存在问题:给ref赋值时存的是item.keyTrigger字符串,不是真实DOM节点,自然无法触发对应元素的交互。下面提供两种可直接复用的实现方案,优先选第一种,逻辑最简单,推荐新手使用,不需要复杂ref操作。

方案1:按键值直接匹配(无ref依赖)

核心逻辑:拿到键盘事件的key值转大写后,直接匹配鼓垫配置,利用你已经设置好的元素id获取DOM节点即可,不需要额外维护ref。

  1. 抽离公共触发逻辑,点击、键盘按下事件复用同一段逻辑,避免重复代码
  2. 移除按钮上重复绑定的onKeyPress——你已经在document层面做了全局监听,重复绑定会导致逻辑触发两次
  3. 播放音频前重置播放进度,支持连续按键连续发声

修改后的完整代码:

// 公共触发逻辑
const triggerDrum = (keyTrigger) => {
  // 匹配对应鼓垫配置
  const targetDrum = bankOne.find(item => item.keyTrigger === keyTrigger);
  if (!targetDrum) return;

  // 播放对应音频
  const audioEl = document.getElementById(keyTrigger);
  if (audioEl) {
    audioEl.currentTime = 0;
    audioEl.play().catch(err => console.log('播放失败:', err));
  }

  // 模拟按键按下样式(可根据自己的UI需求调整类名)
  const btnEl = document.getElementById(targetDrum.id);
  if (btnEl) {
    btnEl.classList.add('active');
    setTimeout(() => btnEl.classList.remove('active'), 100);
  }
}

// 键盘事件处理
const handleKeyPress = React.useCallback((event) => {
  const pressedKey = event.key.toUpperCase();
  triggerDrum(pressedKey);
}, [bankOne]); // 记得把bankOne加入依赖数组

// 点击事件处理
const handleClick = (e) => {
  const keyTrigger = e.currentTarget.value;
  triggerDrum(keyTrigger);
}

// 全局事件绑定逻辑无需修改
React.useEffect(() => {
  document.addEventListener('keypress', handleKeyPress);
  return () => {
    document.removeEventListener('keypress', handleKeyPress);
  };
}, [handleKeyPress]);

列表渲染部分删除错误的ref赋值即可:

const bank1 = bankOne.map((item) => (
  <button
    id={item.id}
    className="drum-pad"
    value={item.keyTrigger}
    onClick={handleClick}
  >
    <audio src={item.url} className="clip" id={item.keyTrigger}>
      <source src={item.url} type="audio/mp3" />
    </audio>
    {item.keyTrigger}
  </button>
));

方案2:对象结构ref存储DOM(适合不想使用原生选择器的场景)

如果更倾向于用ref管理DOM节点,不要用数组索引做存储键(后续数组增删排序时索引会变化,导致匹配错乱),改用keyTrigger作为ref对象的key,直接按键值取对应节点即可。

  1. 初始化ref为普通对象
const itemsRef = React.useRef({});
  1. 渲染列表时,把按钮、音频DOM分别存在ref对象对应字段下
const bank1 = bankOne.map((item) => (
  <button
    ref={el => itemsRef.current[item.keyTrigger] = el}
    id={item.id}
    className="drum-pad"
    value={item.keyTrigger}
    onClick={handleClick}
  >
    <audio
      ref={el => itemsRef.current[`audio-${item.keyTrigger}`] = el}
      src={item.url}
      className="clip"
      id={item.keyTrigger}
    >
      <source src={item.url} type="audio/mp3" />
    </audio>
    {item.keyTrigger}
  </button>
));
  1. 触发逻辑里直接从ref取节点操作
const triggerDrum = (keyTrigger) => {
  const audioEl = itemsRef.current[`audio-${keyTrigger}`];
  const btnEl = itemsRef.current[keyTrigger];
  if (!audioEl || !btnEl) return;

  audioEl.currentTime = 0;
  audioEl.play().catch(err => console.log('播放失败:', err));
  btnEl.classList.add('active');
  setTimeout(() => btnEl.classList.remove('active'), 100);
}

注意事项

  • 如果后续遇到功能键识别失效、中文输入法下按键触发异常的问题,可以把keypress事件换成keydown
  • 音频play方法返回Promise,最好加catch捕获浏览器自动播放限制导致的报错
  • 不要用数组索引作为ref或列表key的标识,避免动态修改数组时出现匹配错误

内容的提问来源于stack exchange,提问作者kerim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 23:36:38