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存在两个小问题:
- ref对象不需要放在
useCallback的依赖数组中 - 读取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
相关产品推荐
相关产品推荐

