基于 React 的倒计时组件实现:暴露方法供父组件状态管理
基于 React 的倒计时组件实现:暴露方法供父组件状态管理
在 React 开发中,倒计时组件是常见的交互元素,常用于验证码倒计时、录制时长限制等场景。本文将实现一个支持父组件获取剩余时间的倒计时组件(CountDown),通过 forwardRef + useImperativeHandle 暴露核心方法,配合父组件完成超时判断与提示逻辑。
一、核心技术点说明
- forwardRef:用于将父组件传递的 ref 转发到子组件内部,实现父组件对於子组件的 DOM 或实例引用。
- useImperativeHandle:自定义暴露给父组件的实例值,避免暴露子组件内部所有 DOM 或状态,仅对外提供必要方法(本文为
getTime方法)。 - 自定义倒计时 Hook(useCountDown):封装倒计时核心逻辑,负责秒数递减、计时停止等功能,使组件职责更单一。
二、完整实现代码
1. 自定义倒计时 Hook(useCountDown.ts)
首先封装倒计时核心逻辑,返回当前剩余时间,简化组件内部代码:
import { useState, useEffect, useRef } from 'react';/*** 自定义倒计时 Hook* @param initialTime 初始倒计时秒数* @returns 剩余时间*/
export const useCountDown = (initialTime: number) => {const [time, setTime] = useState(initialTime);const timerRef = useRef<NodeJS.Timeout | null>(null);// 初始化倒计时useEffect(() => {// 清除之前的定时器,避免重复计时if (timerRef.current) clearInterval(timerRef.current);// 启动倒计时timerRef.current = setInterval(() => {setTime(prev => {// 倒计时结束,清除定时器if (prev <= 1) {clearInterval(timerRef.current!);return 0;}return prev - 1;});}, 1000);// 组件卸载时清除定时器(避免内存泄漏)return () => {if (timerRef.current) clearInterval(timerRef.current);};}, [initialTime]);return { time };
};
2. 倒计时组件(CountDown.tsx)
使用 forwardRef 转发 ref,通过 useImperativeHandle 暴露 getTime 方法,供父组件获取剩余时间:
import { forwardRef, useImperativeHandle } from 'react';
import { useCountDown } from './useCountDown';// 定义组件暴露给父组件的方法类型
interface CountDownExposedMethods {getTime: () => number; // 获取剩余时间的方法
}// 通过 forwardRef 转发父组件传递的 ref
const CountDown = forwardRef<CountDownExposedMethods>((_, ref) => {const { time } = useCountDown(60); // 初始化 60 秒倒计时// 自定义暴露给父组件的方法,依赖 time 状态更新useImperativeHandle(ref, () => ({getTime: () => time, // 返回当前剩余时间}), [time]); // 当 time 变化时,更新暴露的方法(确保获取最新值)// 渲染倒计时样式(补零处理,确保格式统一:01s、02s...60s)return (<div className="count-down text-blue-600 font-medium">{time.toString().padStart(2, '0')} s</div>);
});// 设置组件显示名称(便于调试)
CountDown.displayName = 'CountDown';export default CountDown;
3. 父组件(ParentComponent.tsx)
通过 ref 获取子组件暴露的 getTime 方法,实时获取剩余时间,判断是否超时并显示提示:
import { useRef, useState, useEffect } from 'react';
import CountDown from './CountDown';const ParentComponent = () => {// 创建 ref 用于关联倒计时组件const countDownRef = useRef<{ getTime: () => number }>(null);// 超时状态(控制提示显示)const [isTimeout, setIsTimeout] = useState(false);// 监听倒计时状态,判断是否超时useEffect(() => {// 每 100ms 检查一次剩余时间(避免遗漏超时瞬间)const checkTimer = setInterval(() => {// 确保 ref 已关联组件且存在 getTime 方法if (countDownRef.current) {const remainingTime = countDownRef.current.getTime();// 剩余时间为 0 时,标记为超时if (remainingTime === 0) {setIsTimeout(true);clearInterval(checkTimer); // 超时后停止检查}}}, 100);// 组件卸载时清除检查定时器return () => {clearInterval(checkTimer);};}, []);// 重置倒计时(可选功能,如需重新开始倒计时)const resetCountDown = () => {setIsTimeout(false);// 这里可结合实际需求扩展(如重新渲染组件重置倒计时)// 若需更灵活的重置,可在 CountDown 组件中暴露 reset 方法};return (<div className="parent-container p-8 max-w-md mx-auto"><h3 className="text-xl font-bold mb-4">录制时长限制</h3>{/* 倒计时组件,传递 ref */}<CountDown ref={countDownRef} />{/* 超时提示(超时后显示) */}{isTimeout && (<div className="mt-4 text-red-500 text-sm">已超时,请重新录制</div>)}{/* 重置按钮(超时后显示) */}{isTimeout && (<buttononClick={resetCountDown}className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">重新开始</button>)}</div>);
};export default ParentComponent;
三、关键逻辑解析
-
方法暴露逻辑:
- 子组件通过
forwardRef接收父组件传递的 ref,再通过useImperativeHandle自定义暴露的内容,仅对外提供getTime方法,隐藏内部的time状态和定时器逻辑,符合“最小暴露原则”。 useImperativeHandle的依赖数组包含time,确保每次time更新时,父组件通过getTime获取的都是最新剩余时间。
- 子组件通过
-
超时判断逻辑:
- 父组件通过
useRef创建 ref 并关联子组件,在useEffect中启动定时器,定期调用子组件的getTime方法获取剩余时间。 - 当剩余时间为 0 时,设置
isTimeout为true,显示超时提示,同时清除检查定时器避免无效循环。
- 父组件通过
-
性能与内存优化:
- 子组件中使用
useRef存储定时器实例,在组件卸载和倒计时结束时及时清除,避免内存泄漏。 - 父组件的检查定时器在组件卸载时清除,同样避免内存泄漏。
- 子组件中使用
四、扩展场景
- 自定义初始倒计时:可将
useCountDown的初始时间改为 props 传递,使组件支持动态配置(如props.initialTime)。 - 暂停/继续功能:在
useCountDown中扩展pause和resume方法,通过useImperativeHandle暴露给父组件,实现倒计时的暂停与继续。 - 格式自定义:支持父组件传递格式函数(如
formatTime: (time: number) => string),自定义倒计时显示格式(如mm:ss)。
五、总结
本文通过 forwardRef + useImperativeHandle 实现了父子组件的方法通信,使父组件能够灵活获取倒计时组件的剩余时间并进行超时处理。这种方式既保证了子组件的封装性,又满足了父组件的状态管理需求,适用于各类需要父子组件协作的倒计时场景。同时,通过自定义 Hook 封装核心逻辑,使代码结构更清晰、可维护性更强。
