如何在useEffect内设置state时避免useRef被重置?
问题描述
实现了一个仅支持水平拖拽的滑块组件,拖动滑块时会修改表单对应的selectedPlanDetails状态,但每次设置该状态都会触发父页面重渲染,导致滑块组件内部的useRef被重置,拖拽功能异常。
解决方案
核心问题是**DraggableComponent定义在父组件内部**,父组件每次重渲染时都会重新创建这个组件,导致其内部的ref、state全部被重置。同时频繁更新position状态也会加剧重渲染问题,以下是具体修复步骤:
1. 将滑块组件移到父组件外部
把DraggableComponent从FinancialLandingPage内部提取出来,避免父组件重渲染时重新定义组件,保证其内部状态和ref的稳定性。
2. 通过props传递依赖项
将父组件的setSelectedPlanDetails、翻译函数t、base_t等需要的参数通过props传入子组件,确保子组件能正常更新父组件状态并获取翻译文本。
3. 优化拖拽状态管理(可选)
用useRef存储拖拽过程中的临时状态(如pressed、position),避免频繁更新state触发重渲染,仅在需要更新表单状态时才修改父组件的selectedPlanDetails。
4. 用React.memo优化组件
给DraggableComponent包裹React.memo,减少不必要的重渲染。
修改后的完整代码
import { useState, useEffect, useRef, useCallback, memo } from 'react'; import { useTranslation } from 'react-i18next'; // 提取滑块组件到外部 const DraggableComponent = memo(({ setSelectedPlanDetails, t, base_t }) => { const pressedRef = useRef(false); const positionRef = useRef({ x: 0, y: 0 }); const [currentPlan, setCurrentPlan] = useState(1); const ref = useRef(); // 监听position变化更新DOM和表单状态 useEffect(() => { const updatePlan = () => { const x = positionRef.current.x; if (x >= 0 && x < 147) { setCurrentPlan(1); setSelectedPlanDetails(SUBSCRIPTION_PLANS.newbie.name); } else if (x >= 147 && x < 294) { setCurrentPlan(2); setSelectedPlanDetails(SUBSCRIPTION_PLANS.enthusiast.name); } else if (x >= 294 && x < 441) { setCurrentPlan(3); setSelectedPlanDetails(SUBSCRIPTION_PLANS.trader.name); } else if (x >= 441 && x <= 600) { setCurrentPlan(4); setSelectedPlanDetails(SUBSCRIPTION_PLANS.traderplus.name); } }; // 更新DOM位置 if (ref.current) { ref.current.style.transform = `translate(${positionRef.current.x}px, 0)`; } updatePlan(); }, [setSelectedPlanDetails]); // 鼠标移动事件,用ref存储状态避免依赖更新 const onMouseMove = useCallback((event) => { if (!pressedRef.current) return; let newX = positionRef.current.x + event.movementX; // 限制拖拽范围 newX = Math.max(0, Math.min(newX, 600)); positionRef.current.x = newX; // 手动触发DOM更新和表单状态检查 if (ref.current) { ref.current.style.transform = `translate(${newX}px, 0)`; } // 仅当计划切换时更新state,减少重渲染 const currentPlanIndex = Math.floor(newX / 147) + 1; if (currentPlanIndex !== currentPlan) { setCurrentPlan(currentPlanIndex); const planKeys = ['newbie', 'enthusiast', 'trader', 'traderplus']; setSelectedPlanDetails(SUBSCRIPTION_PLANS[planKeys[currentPlanIndex - 1]].name); } }, [currentPlan, setSelectedPlanDetails]); const handleMouseDown = () => { pressedRef.current = true; }; const handleMouseUp = () => { pressedRef.current = false; }; return ( <div ref={ref} onMouseMove={onMouseMove} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} // 增加鼠标离开时释放状态 > <div className="slider-bar-item"> <Heading level="5">{t(base_t + "page_five.slider").replace("[num]", currentPlan)}</Heading> </div> </div> ); }); const FinancialLandingPage = () => { const { t } = useTranslation(); const [selectedPlanDetails, setSelectedPlanDetails] = useState(SUBSCRIPTION_PLANS.newbie.name); return ( <> <div className="slider"> <div className="slider-bar"> <DraggableComponent setSelectedPlanDetails={setSelectedPlanDetails} t={t} base_t={base_t} // 假设base_t是外部定义的常量 /> </div> </div> <div className="form-container"> <div className="header"> <Heading align="center" level="3"> {t(base_t + "page_five.form.title")} </Heading> <Heading align="center" level="1"> {t(base_t + `page_five.form.${selectedPlanDetails}.price`)} </Heading> <Heading align="center" level="6"> {t(base_t + "page_five.form.price_description")} </Heading> </div> <div className="form"> <div className="form-item"> <CryptoIcon name="checkmark" /> <Text size={16}> {t(base_t + `page_five.form.${selectedPlanDetails}.item_one`)} </Text> </div> <div className="form-item"> <CryptoIcon name="checkmark" /> <Text size={16}> {t(base_t + `page_five.form.${selectedPlanDetails}.item_two`)} </Text> </div> <div className="form-item"> <CryptoIcon name="checkmark" /> <Text size={16}> {__(base_t + `page_five.form.${selectedPlanDetails}.item_three`)} </Text> </div> <Button className="button"> {t(base_t + "page_five.form.button")} <Icon name="chevron_right" /> </Button> </div> </div> </> ); }; export default FinancialLandingPage;
关键说明
- 组件提取后,内部的
ref和state不会因父组件重渲染而重置,保证拖拽过程的连续性。 - 用
useRef存储pressed和position,避免频繁更新state导致的重渲染,提升拖拽流畅度。 React.memo会对比props的变化,仅在必要时才重渲染滑块组件。- 增加
onMouseLeave事件,避免鼠标移出滑块区域后仍保持拖拽状态。
内容的提问来源于stack exchange,提问作者Asher
相关产品推荐
相关产品推荐

