基于 React 的倒计时组件实现:暴露方法供父组件状态管理

在 React 开发中,倒计时组件是常见的交互元素,常用于验证码倒计时、录制时长限制等场景。本文将实现一个支持父组件获取剩余时间的倒计时组件(CountDown),通过 forwardRef + useImperativeHandle 暴露核心方法,配合父组件完成超时判断与提示逻辑。

一、核心技术点说明

  1. forwardRef:用于将父组件传递的 ref 转发到子组件内部,实现父组件对於子组件的 DOM 或实例引用。
  2. useImperativeHandle:自定义暴露给父组件的实例值,避免暴露子组件内部所有 DOM 或状态,仅对外提供必要方法(本文为 getTime 方法)。
  3. 自定义倒计时 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 && (
        <button
          onClick={resetCountDown}
          className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
        >
          重新开始
        </button>
      )}
    </div>
  );
};

export default ParentComponent;

三、关键逻辑解析

  1. 方法暴露逻辑

    • 子组件通过 forwardRef 接收父组件传递的 ref,再通过 useImperativeHandle 自定义暴露的内容,仅对外提供 getTime 方法,隐藏内部的 time 状态和定时器逻辑,符合“最小暴露原则”。
    • useImperativeHandle 的依赖数组包含 time,确保每次 time 更新时,父组件通过 getTime 获取的都是最新剩余时间。
  2. 超时判断逻辑

    • 父组件通过 useRef 创建 ref 并关联子组件,在 useEffect 中启动定时器,定期调用子组件的 getTime 方法获取剩余时间。
    • 当剩余时间为 0 时,设置 isTimeouttrue,显示超时提示,同时清除检查定时器避免无效循环。
  3. 性能与内存优化

    • 子组件中使用 useRef 存储定时器实例,在组件卸载和倒计时结束时及时清除,避免内存泄漏。
    • 父组件的检查定时器在组件卸载时清除,同样避免内存泄漏。

四、扩展场景

  1. 自定义初始倒计时:可将 useCountDown 的初始时间改为 props 传递,使组件支持动态配置(如 props.initialTime)。
  2. 暂停/继续功能:在 useCountDown 中扩展 pauseresume 方法,通过 useImperativeHandle 暴露给父组件,实现倒计时的暂停与继续。
  3. 格式自定义:支持父组件传递格式函数(如 formatTime: (time: number) => string),自定义倒计时显示格式(如 mm:ss)。

五、总结

本文通过 forwardRef + useImperativeHandle 实现了父子组件的方法通信,使父组件能够灵活获取倒计时组件的剩余时间并进行超时处理。这种方式既保证了子组件的封装性,又满足了父组件的状态管理需求,适用于各类需要父子组件协作的倒计时场景。同时,通过自定义 Hook 封装核心逻辑,使代码结构更清晰、可维护性更强。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐