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

React17.0.2及以上版本如何实现无钩子渲染数量报错的长按功能

问题复现

现有React长按实现方案均无法在React 17.0.2版本正常运行,统一抛出如下错误:

Error: Rendered more hooks than during the previous render.

即使按照通用修复方案将useState替换为useRef也无法解决,需要适配React 17.0.2及更高版本的长点击/长按/触屏长按功能。

现有尝试代码如下:

自定义Hook useLongPress.js

import {useCallback, useRef, useState} from "react";

const useLongPress = (
    onLongPress,
    onClick,
    {shouldPreventDefault = true, delay = 300} = {}
) => {
    //const [longPressTriggered, setLongPressTriggered] = useState(false);
    const longPressTriggered = useRef(false);
    const timeout = useRef();
    const target = useRef();
    
    const start = useCallback(
        event => {
            if (shouldPreventDefault && event.target) {
                event.target.addEventListener("touchend", preventDefault, {
                    passive: false
                });
                target.current = event.target;
            }
            timeout.current = setTimeout(() => {
                onLongPress(event);
                //setLongPressTriggered(true);
                longPressTriggered.current = true;
            }, delay);
        },
        [onLongPress, delay, shouldPreventDefault]
    );
    
    const clear = useCallback(
        (event, shouldTriggerClick = true) => {
            timeout.current && clearTimeout(timeout.current);
            shouldTriggerClick && !longPressTriggered && onClick(event);
            //setLongPressTriggered(false);
            longPressTriggered.current = false;
            if (shouldPreventDefault && target.current) {
                target.current.removeEventListener("touchend", preventDefault);
            }
        },
        [shouldPreventDefault, onClick, longPressTriggered]
    );
    
    return {
        onMouseDown: e => start(e),
        onTouchStart: e => start(e),
        onMouseUp: e => clear(e),
        onMouseLeave: e => clear(e, false),
        onTouchEnd: e => clear(e)
    };
};

const isTouchEvent = event => {
    return "touches" in event;
};

const preventDefault = event => {
    if (!isTouchEvent(event)) return;
    
    if (event.touches.length < 2 && event.preventDefault) {
        event.preventDefault();
    }
};

export default useLongPress;

业务组件 RandomItem.js

import React, {useEffect, useState} from 'react';
import Item from "../components/Item";
import Loader from "../../shared/components/UI/Loader";
import {useAxiosGet} from "../../shared/hooks/HttpRequest";
import useLongPress from '../../shared/hooks/useLongPress';


function RandomItem() {
    let content = null;
    let item = useAxiosGet('collection');
    
    if (item.error === true) {
        content = <p>获取随机条目时出现错误</p>;
    }
    
    if (item.loading === true) {
        content = <Loader/>;
    }
    
    if (item.data) {
        const onLongPress = useLongPress();
        return (
            content =
                <div>
                    <h1 className="text-6xl font-normal leading-normal mt-0 mb-2">{item.data.name}</h1>
                    <Item name={item.data.name} image={item.data.filename} description={item.data.description}/>
                </div>
        )
    }
    
    return (
        <div>
            {content}
        </div>
    );
}

export default RandomItem;

根因分析

报错和React版本、长按Hook实现无关,是违反了React Hook调用规则:所有Hook必须在组件顶层作用域调用,不能包裹在条件判断、循环或嵌套函数中。你将useLongPress()放在if (item.data)分支内,接口加载、报错阶段不会调用该Hook,数据返回后才调用,两次渲染的Hook数量不一致就会抛出该错误。
同时原useLongPress存在两个小问题:

  1. ref对象不需要放在useCallback的依赖数组中
  2. 读取ref值时需要取.current属性,原判断逻辑!longPressTriggered永远为假,点击事件无法正常触发

修复后代码

修复后的useLongPress.js

import {useCallback, useRef} from "react";

const useLongPress = (
  onLongPress,
  onClick,
  { shouldPreventDefault = true, delay = 300 } = {}
) => {
  const longPressTriggered = useRef(false);
  const timeout = useRef();
  const target = useRef();

  const start = useCallback(
    (event) => {
      if (shouldPreventDefault && event.target) {
        event.target.addEventListener("touchend", preventDefault, {
          passive: false,
        });
        target.current = event.target;
      }
      timeout.current = setTimeout(() => {
        onLongPress(event);
        longPressTriggered.current = true;
      }, delay);
    },
    [onLongPress, delay, shouldPreventDefault]
  );

  const clear = useCallback(
    (event, shouldTriggerClick = true) => {
      timeout.current && clearTimeout(timeout.current);
      if (shouldTriggerClick && !longPressTriggered.current) {
        onClick?.(event);
      }
      longPressTriggered.current = false;
      if (shouldPreventDefault && target.current) {
        target.current.removeEventListener("touchend", preventDefault);
      }
    },
    [shouldPreventDefault, onClick]
  );

  return {
    onMouseDown: (e) => start(e),
    onTouchStart: (e) => start(e),
    onMouseUp: (e) => clear(e),
    onMouseLeave: (e) => clear(e, false),
    onTouchEnd: (e) => clear(e),
  };
};

const isTouchEvent = (event) => {
  return "touches" in event;
};

const preventDefault = (event) => {
  if (!isTouchEvent(event)) return;
  if (event.touches.length < 2 && event.preventDefault) {
    event.preventDefault();
  }
};

export default useLongPress;

修复后的RandomItem.js

import React from 'react';
import Item from "../components/Item";
import Loader from "../../shared/components/UI/Loader";
import {useAxiosGet} from "../../shared/hooks/HttpRequest";
import useLongPress from '../../shared/hooks/useLongPress';

function RandomItem() {
  // 所有Hook移到组件顶层
  const item = useAxiosGet('collection');
  // 先定义长按和点击的回调逻辑,再传给useLongPress
  const handleLongPress = (e) => {
    // 此处写长按触发后的业务逻辑
    console.log('长按触发', e);
  };
  const handleClick = (e) => {
    // 此处写普通点击触发后的业务逻辑
    console.log('普通点击触发', e);
  };
  const longPressHandlers = useLongPress(handleLongPress, handleClick);

  let content = null;
  if (item.error) {
    content = <p>获取随机条目时出现错误</p>;
  } else if (item.loading) {
    content = <Loader/>;
  } else if (item.data) {
    content = (
      <div>
        <h1 className="text-6xl font-normal leading-normal mt-0 mb-2">{item.data.name}</h1>
        {/* 把长按事件绑定到需要触发的元素上,比如Item组件 */}
        <Item 
          {...longPressHandlers}
          name={item.data.name} 
          image={item.data.filename} 
          description={item.data.description}
        />
      </div>
    )
  }

  return <div>{content}</div>;
}

export default RandomItem;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 18:51:02