新闻中心

React中子组件向父组件传递状态:实现倒计时结束通知

2025-12-13
浏览次数:
返回列表

react中子组件向父组件传递状态:实现倒计时结束通知

本教程详细讲解了如何在React应用中实现子组件向父组件传递状态。通过一个倒计时组件的实际案例,演示了“状态提升”(Lifting State Up)这一核心React模式,即父组件管理状态并通过props将更新函数传递给子组件,从而实现子组件对父组件状态的修改,进而控制父组件的渲染逻辑。

在React应用开发中,组件之间的数据流通常是单向的,即从父组件流向子组件。然而,在许多场景下,子组件需要通知父组件某个事件的发生或状态的变化,例如一个表单子组件需要将用户输入传递给父组件,或者一个交互式子组件需要告知父组件其内部状态的更新。本文将以一个具体的倒计时组件为例,详细阐述如何通过“状态提升”(Lifting State Up)模式,实现子组件向父组件传递状态,进而控制父组件的渲染逻辑。

理解子组件向父组件通信的需求

假设我们有一个CountDown子组件,它负责显示一个倒计时。当倒计时归零时,我们希望它的父组件QuestionCard能够感知到这一变化,并根据倒计时是否结束来决定渲染不同的内容(例如,倒计时未结束时显示问题和答案,倒计时结束后显示一个提示信息)。

最初的实现中,CountDown组件内部维护了一个onTime状态来表示倒计时是否仍在进行。然而,这个状态是局限于CountDown组件内部的,父组件QuestionCard无法直接访问或监听这个状态。为了实现父组件根据子组件状态进行条件渲染的需求,我们需要一种机制让CountDown能够修改QuestionCard中的相关状态。

“状态提升”模式简介

“状态提升”是React中处理组件间共享状态的推荐模式。其核心思想是:当多个组件需要共享或响应同一个状态时,将这个状态提升到它们最近的共同父组件中管理。然后,父组件通过props将状态以及更新状态的函数传递给子组件。子组件通过调用这些函数来修改父组件中的状态。

这种模式确保了状态的单一数据源,使得应用的数据流更加清晰和可预测。

实现步骤:将onTime状态提升至父组件

为了解决上述问题,我们将onTime状态从CountDown子组件提升到QuestionCard父组件。

OpenAI Codex OpenAI Codex

可以生成十多种编程语言的工作代码,基于 OpenAI GPT-3 的自然语言处理模型

OpenAI Codex 144 查看详情 OpenAI Codex

1. 父组件 QuestionCard 管理 onTime 状态

首先,在QuestionCard组件中声明一个onTime状态,并初始化为true。同时,QuestionCard需要将setOnTime这个状态更新函数作为prop传递给CountDown子组件。

import React, { useEffect, useState } from 'react';
import {
  Grid,
  Box,
  Card,
  CardContent,
  CardActions,
  Typography,
  ButtonGroup,
  ListItemButton,
  Button,
  LinearProgress,
} from '@mui/material';
// 假设 useAxios 和 baseURL_Q 已定义
// import useAxios from './hooks/useAxios'; 
// const baseURL_Q = 'your_api_url'; 

import CountDown from './CountDown'; // 确保路径正确

export default function QuestionCard() {
  const [questions, setQuestions] = useState([]);
  const [clickedIndex, setClickedIndex] = useState(0);
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
  const [value, setValue] = useState(null);
  const [onTime, setOnTime] = useState(true); // 将 onTime 状态提升到父组件

  // 假设 useAxios 及其 sendRequest 方法已正确实现
  const { isLoading, error, sendRequest: getQuestions } = { isLoading: false, error: null, sendRequest: () => {} }; // 模拟
  const { sendRequest: getAnswers } = { sendRequest: () => {} }; // 模拟

  const handleSubmit = () => {
    setValue(true);
  };

  const handleSelectedItem = (index) => {
    setClickedIndex(index);
  };

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  // 模拟 goToNext 函数
  const goToNext = () => {
    setCurrentQuestionIndex((prevIndex) => (prevIndex + 1) % questions.length);
    setValue(null); // 重置选择
    setClickedIndex(0); // 重置点击
  };

  useEffect(() => {
    const transformQuestions = (questionObj) => {
      const loadedQuestions = [];
      for (const questionKey in questionObj) {
        loadedQuestions.push({
          id: questionKey,
          id_test: questionObj[questionKey].id_test,
          tipologia_domanda: questionObj[questionKey].tipologia_domanda,
          testo: questionObj[questionKey].testo,
          immagine: questionObj[questionKey].immagine,
          eliminata: questionObj[questionKey].eliminata,
        });
      }
      setQuestions(loadedQuestions);
    };
    // 模拟 API 调用
    // getQuestions(
    //   {
    //     method: 'GET',
    //     url: baseURL_Q,
    //   },
    //   transformQuestions
    // );
    // 假设加载一些模拟数据
    setQuestions([
      { id: 'q1', testo: '这是第一个问题?', id_test: 't1', tipologia_domanda: '单选', immagine: null, eliminata: false },
      { id: 'q2', testo: '这是第二个问题?', id_test: 't1', tipologia_domanda: '单选', immagine: null, eliminata: false },
    ]);
  }, [getQuestions]);

  let questionsTitle = questions.map((element) => `${element.testo}`);
  // let questionId = questions.map((element) => `${element.id}`); // 未使用

  return (
    <Grid container spacing={1}>
      <Grid item xs={10}>
        <Box
          sx={{
            minWidth: 275,
            display: 'flex',
            alignItems: 'center',
            paddingLeft: '50%',
            paddingBottom: '5%',
            position: 'center',
          }}
        >
          <Card
            variant='outlined'
            sx={{
              minWidth: 400,
            }}
          >
            <CardContent>
              <Grid container spacing={0}>
                <Grid item xs={8}>
                  <Typography
                    variant='h5'
                    component='div'
                    fontFamily={'Roboto'}
                  >
                    Nome Test
                  </Typography>
                </Grid>
                <Grid item xs={4}>
                  {/* 将 setOnTime 函数作为 prop 传递给 CountDown */}
                  <CountDown seconds={30} setOnTime={setOnTime} /> 
                </Grid>
              </Grid>

              <LinearProgress variant='determinate' value={1} />

              {/* 根据 onTime 状态进行条件渲染 */}
              {onTime ? (
                <>
                  <Typography
                    sx={{ mb: 1.5, mt: 1.5 }}
                    fontFamily={'Roboto'}
                    fontWeight={'bold'}
                  >
                    {questionsTitle[currentQuestionIndex]}
                  </Typography>

                  <ButtonGroup
                    fullWidth
                    orientation='vertical'
                    onClick={handleSubmit}
                    onChange={handleChange}
                  >
                    <ListItemButton
                      selected={clickedIndex === 1}
                      onClick={() => handleSelectedItem(1)}
                    >
                      Risposta 1
                    </ListItemButton>
                    <ListItemButton
                      selected={clickedIndex === 2}
                      onClick={() => handleSelectedItem(2)}
                    >
                      Risposta 2
                    </ListItemButton>
                    <ListItemButton
                      selected={clickedIndex === 3}
                      onClick={() => handleSelectedItem(3)}
                    >
                      Risposta 3
                    </ListItemButton>
                    <ListItemButton
                      selected={clickedIndex === 4}
                      onClick={() => handleSelectedItem(4)}
                    >
                      Risposta 4
                    </ListItemButton>
                  </ButtonGroup>
                </>
              ) : (
                <Typography variant='h6' color='error' sx={{ mt: 2 }}>
                  时间已到!
                </Typography>
              )}
            </CardContent>
            <CardActions>
              <Button onClick={goToNext} disabled={!value || !onTime} variant='contained' size='small'>
                Avanti
              </Button>
            </CardActions>
          </Card>
        </Box>
      </Grid>
    </Grid>
  );
}

在QuestionCard中,我们现在有了onTime状态。当onTime为true时,渲染问题和答案;当onTime为false时,渲染“时间已到!”的提示信息。

2. 子组件 CountDown 触发父组件状态更新

CountDown组件不再需要维护自己的onTime状态。它将通过调用从父组件接收到的props.setOnTime函数来更新父组件的onTime状态。

import { Typography, Paper, Grid } from '@mui/material';
import React, { useEffect, useRef, useState } from 'react';

const formatTime = (time) => {
  let minutes = Math.floor(time / 60);
  let seconds = Math.floor(time - minutes * 60);

  // 确保分钟和秒数至少是两位数
  minutes = minutes < 10 ? '0' + minutes : minutes;
  seconds = seconds < 10 ? '0' + seconds : seconds;

  return minutes + ':' + seconds;
};

function CountDown(props) {
  const [countdown, setCountdown] = useState(props.seconds);
  const timertId = useRef();

  useEffect(() => {
    // 设置定时器,每秒更新 countdown 状态
    timertId.current = setInterval(() => {
      setCountdown((prev) => prev - 1);
    }, 1000);

    // 清理函数:组件卸载时清除定时器
    return () => clearInterval(timertId.current);
  }, []); // 空依赖数组确保定时器只设置一次

  useEffect(() => {
    // 当 countdown 归零时
    if (countdown <= 0) {
      clearInterval(timertId.current); // 清除定时器
      props.setOnTime(false); // 调用父组件传递的函数,更新父组件的 onTime 状态为 false
    }
  }, [countdown, props.setOnTime]); // 依赖 countdown 和 setOnTime

  return (
    <Grid container>
      <Grid item xs={5}>
        <Paper elevation={0} variant='outlined' square>
          <Typography component='h6' fontFamily={'Roboto'}>
            Timer:
          </Typography>
        </Paper>
      </Grid>
      <Grid item xs={5}>
        <Paper
          elevation={0}
          variant='outlined'
          square
          sx={{ bgcolor: 'lightblue' }}
        >
          <Typography component='h6' fontFamily={'Roboto'}>
            {formatTime(countdown)}
          </Typography>
        </Paper>
      </Grid>
    </Grid>
  );
}

export default CountDown;

在CountDown组件中:

  • 移除了本地的onTime状态。
  • 在第二个useEffect中,当countdown小于等于0时,除了清除定时器,还调用了从props接收到的setOnTime(false)函数。
  • useEffect的依赖数组中包含了props.setOnTime。虽然setOnTime函数在组件的整个生命周期中通常是稳定的(React保证了useState返回的setter函数是稳定的),但将其包含在依赖数组中是最佳实践,以避免潜在的linting警告或未来React版本行为变化的影响。

完整代码示例

通过上述修改,CountDown子组件不再拥有自己的onTime状态,而是通过props.setOnTime函数直接修改父组件QuestionCard中的onTime状态。当倒计时结束时,QuestionCard的onTime状态会变为false,从而触发QuestionCard的重新渲染,并显示“时间已到!”的提示。

QuestionCard.jsx (父组件)

import React, { useEffect, useState } from 'react';
import {
  Grid,
  Box,
  Card,
  CardContent,
  CardActions,
  Typography,
  ButtonGroup,
  ListItemButton,
  Button,
  LinearProgress,
} from '@mui/material';
// import useAxios from './hooks/useAxios'; // 假设 useAxios 已定义
// const baseURL_Q = 'your_api_url'; 

import CountDown from './CountDown';

export default function QuestionCard() {
  const [questions, setQuestions] = useState([]);
  const [clickedIndex, setClickedIndex] = useState(0);
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
  const [value, setValue] = useState(null);
  const [onTime, setOnTime] = useState(true); // 状态提升至父组件

  // 模拟 useAxios 及其 sendRequest 方法
  const { isLoading, error, sendRequest: getQuestions } = { isLoading: false, error: null, sendRequest: () => {} };
  const { sendRequest: getAnswers } = { sendRequest: () => {} };

  const handleSubmit = () => {
    setValue(true);
  };

  const handleSelectedItem = (index) => {
    setClickedIndex(index);
  };

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  const goToNext = () => {
    setCurrentQuestionIndex((prevIndex) => (prevIndex + 1) % questions.length);
    setValue(null); 
    setClickedIndex(0); 
  };

  useEffect(() => {
    const transformQuestions = (questionObj) => {
      const loadedQuestions = [];
      for (const questionKey in questionObj) {
        loadedQuestions.push({
          id: questionKey,
          id_test: questionObj[questionKey].id_test,
          tipologia_domanda: questionObj[questionKey].tipologia_domanda,
          testo: questionObj[questionKey].testo,
          immagine: questionObj[questionKey].immagine,
          eliminata: questionObj[questionKey].eliminata,
        });
      }
      setQuestions(loadedQuestions);
    };
    // 模拟 API 调用
    // getQuestions(
    //   {
    //     method: 'GET',
    //     url: baseURL_Q,
    //   },
    //   transformQuestions
    // );
    setQuestions([
      { id: 'q1', testo: '这是第一个问题?', id_test: 't1', tipologia_domanda: '单选', immagine: null, eliminata: false },
      { id: 'q2', testo: '这是第二个问题?', id_test: 't1', tipologia_domanda: '单选', immagine: null, eliminata: false },
    ]);
  }, [getQuestions]);

  let questionsTitle = questions.map((element) => `${element.testo}`);

  return (
    <Grid container spacing={1}>
      <Grid item xs={10}>
        <Box
          sx={{
            minWidth: 275,
            display: 'flex',
            alignItems: 'center',
            paddingLeft: '50%',
            paddingBottom: '5%',
            position: 'center',
          }}
        >
          <Card
            variant='outlined'
            sx={{
              minWidth: 400,
            }}
          >
            <CardContent>
              <Grid container spacing={0}>
                <Grid item xs={8}>
                  <Typography
                    variant='h5'
                    component='div'
                    fontFamily={'Roboto'}
                  >
                    Nome Test
                  </Typography>
                </Grid>
                <Grid item xs={4}>
                  {/* 传递 setOnTime 函数 */}
                  <CountDown seconds={30} setOnTime={setOnTime} />
                </Grid>
              </Grid>

              <LinearProgress variant='determinate' value={1} />

              {/* 根据 onTime 状态进行条件渲染 */}
              {onTime ? (
                <>
                  <Typography
                    sx={{ mb: 1.5, mt: 1.5 }}
                    fontFamily={'Roboto'}
                    fontWeight={'bold'}
                  >
                    {questionsTitle[currentQuestionIndex]}
                  </Typography>

                  <ButtonGroup
                    fullWidth
                    orientation='vertical'
                    onClick={handleSubmit}
                    onChange={handleChange}
                  >
                    <ListItemButton
                      selected={clickedIndex === 1}
                      onClick={() => handleSelectedItem(1)}
                    >
                      Risposta 1
                    </ListItemButton>
                    <ListItemButton
                      selected={clickedIndex === 2}
                      onClick={() => handleSelectedItem(2)}
                    >
                      Risposta 2
                    </ListItemButton>
                    <ListItemButton
                      selected={clickedIndex === 3}
                      onClick={() => handleSelectedItem(3)}
                    >
                      Risposta 3
                    </ListItemButton>
                    <ListItemButton
                      selected={clickedIndex === 4}
                      onClick={() => handleSelectedItem(4)}
                    >
                      Risposta 4
                    </ListItemButton>
                  </ButtonGroup>
                </>
              ) : (
                <Typography variant='h6' color='error' sx={{ mt: 2 }}>
                  时间已到!
                </Typography>
              )}
            </CardContent>
            <CardActions>
              <Button onClick={goToNext} disabled={!value || !onTime} variant='contained' size='small'>
                Avanti
              </Button>
            </CardActions>
          </Card>
        </Box>
      </Grid>
    </Grid>
  );
}

CountDown.jsx (子组件)

import { Typography, Paper, Grid } from '@mui/material';
import React, { useEffect, useRef, useState } from 'react';

const formatTime = (time) => {
  let minutes = Math.floor(time / 60);
  let seconds = Math.floor(time - minutes * 60);

  minutes = minutes < 10 ? '0' + minutes : minutes;
  seconds = seconds < 10 ? '0' + seconds : seconds;

  return minutes + ':' + seconds;
};

function CountDown(props) {
  const [countdown, setCountdown] = useState(props.seconds);
  const timertId = useRef();

  useEffect(() => {
    timertId.current = setInterval(() => {
      setCountdown((prev) => prev - 1);
    }, 1000);

    return () => clearInterval(timertId.current);
  }, []);

  useEffect(() => {
    if (countdown <= 0) {
      clearInterval(timertId.current);
      props.setOnTime(false); // 调用父组件的更新函数
    }
  }, [countdown, props.setOnTime]); // 依赖 countdown 和 setOnTime

  return (
    <Grid container>
      <Grid item xs={5}>
        <Paper elevation={0} variant='outlined' square>
          <Typography component='h6' fontFamily={'Roboto'}>
            Timer:
          </Typography>
        </Paper>
      </Grid>
      <Grid item xs={5}>
        <Paper
          elevation={0}
          variant='outlined'
          square
          sx={{ bgcolor: 'lightblue' }}
        >
          <Typography component='h6' fontFamily={'Roboto'}>
            {formatTime(countdown)}
          </Typography>
        </Paper>
      </Grid>
    </Grid>
  );
}

export default CountDown;

注意事项与最佳实践

  1. 状态的单一数据源:通过状态提升,onTime状态现在只由QuestionCard管理,避免了状态的重复或不一致。
  2. 函数作为 Prop:将状态更新函数(如setOnTime)作为prop传递给子组件是实现子组件向父组件通信的标准方式。
  3. useEffect依赖数组:在CountDown组件的第二个useEffect中,将props.setOnTime添加到依赖数组是推荐的做法。尽管useState返回的setter函数通常是稳定的,但显式声明依赖可以确保代码的健壮性和可维护性。
  4. 性能优化(useCallback):对于更复杂的应用,如果传递给子组件的函数是动态生成的(例如,依赖于父组件的某些状态),并且子组件被频繁渲染,可以使用useCallback钩子来记忆化这个函数,防止不必要的子组件重新渲染。在这个简单的案例中,setOnTime是useState返回的稳定函数,所以通常不需要useCallback。
  5. 替代方案:对于组件层级较深或状态需要在多个不直接关联的组件间共享的场景,可以考虑使用React Context API、Redux、Zustand等全局状态管理

以上就是React中子组件向父组件传递状态:实现倒计时结束通知的详细内容,更多请关注其它相关文章!


# 这一  # 设计公司seo推广  # 个人网站建设评价模板范文  # 五华网站建设哪家靠谱  # hyein seo 裤  # 安阳网站推广多少钱一年  # 沧州网站建设情况  # 沈阳seo源码  # 防冻液品牌营销推广策略  # 蚌埠抖音推广营销招聘  # 东至网站建设费用情况  # 零时  # 多个  # 第一个  # react  # 自己的  # 单选  # 已到  # 第二个  # 这是  # 倒计时  # red  # 应用开发  # ios  # ai  # axios  # go  # js 


相关栏目: 【 科技资讯46185 】 【 网络学院92790


相关推荐: 如何创建独立于主系统的J*a运行环境_隔离式环境搭建策略  sublime如何处理大型CSV文件的列对齐_sublime高级表格编辑插件指南  如何使用Rector自动化升级旧代码_通过Composer安装和配置Rector进行代码重构  html5 app怎么运行环境_配html5 app运行环境【教程】  uc浏览器网页版极速入口 uc网页浏览器网页版流畅体验  C++如何打印当前代码行号与文件名_C++预定义宏FILE与LINE的使用  jQuery Mask 插件中实现电话号码固定前导零的教程  网易大神怎么保存别人动态的图片_网易大神动态图片保存方法  在J*a中如何使用Stream.map转换元素_Stream映射操作解析  win11开机启动修复循环怎么办 Win11无法进入系统高级启动解决方法【修复】  拼多多赚钱渠道_拼多多收益来源  如何在J*a中使用Locale处理多语言环境  J*aScript对象创建方式_J*aScript设计模式应用  windows10怎么查看硬盘序列号_windows10硬盘id查询命令  狙击外星人小游戏开始_狙击外星人小游戏立即开始  小红书商家版怎样在笔记嵌入商品卡路径_小红书商家版在笔记嵌入商品卡路径【挂载教程】  PHP中高效并行检查多链接状态的教程  必由学官方登录入口 必由学教师学生账号快速访问  React列表渲染与独立状态管理:避免全局状态影响局部更新  J*aScript中正确使用querySelectorAll与复杂CSS选择器  微博网页版主页入口 微博官方网站免登录访问  J*aScript井字棋(Tic-Tac-Toe)核心交互逻辑实现教程  excel怎么制作工资条 excel快速生成工资条的方法  React项目中导航栏Logo自适应布局:避免裁剪与布局溢出  sublime怎么设置启动时打开的窗口_sublime会话管理与热退出  微博网页版怎么开启两步验证_微博网页版账号安全两步验证设置方法  windows10怎么关闭系统提示音_windows10彻底静音设置方法  神经网络二分类模型训练异常:高损失与完美验证准确率的排查与修正  处理动态列数据:J*a ArrayList的正确初始化与字符累加教程  NVIDIA股价11月重挫12%:下月有望好转 但难回5万亿美元巅峰  Python getattr() 异常处理深度解析:避免程序意外退出  ExcelARRAYTOTEXT函数怎么自定义分隔符输出数组文本_ARRAYTOTEXT实现动态生成SQL语句  J*a里如何实现订单支付与库存同步功能_支付库存同步项目开发方法说明  一加Ace 6T支持全新明眸护眼:通过了最严苛的护眼小金标认证  Vue.js 图片显示异常排查:理解应用挂载范围与DOM ID唯一性  b站赚钱渠道_b站收益来源  c++20的std::jthread是什么_c++可中断线程与RAII式管理  C++指针和引用有什么区别_C++内存管理核心概念深度解析  J*aScript中如何高效提取对象指定属性  单12V-2&#215;6实现为RTX 5090供电750W!甚至都没敢跑分  msn官网入口地址手机版 msn官方网站手机最新链接  NRF24L01数据传输深度解析:解决大载荷接收异常与分包策略  使用Python高效删除Word宏并转换DOCM为DOCX格式  win11如何卸载Windows更新补丁 Win11解决更新导致系统不稳定的问题【修复】  Golang如何优化内存分配与垃圾回收_Golang内存管理与GC优化实践  天猫双十一预售商品怎么退款_天猫双十一预售退款操作指南  Golang切片为何属于引用类型_Golang slice底层结构与引用语义说明  如何优雅地扩展SprykerGlue后端API授权逻辑,使用spryker/glue-backend-api-application-authorization-connector-extension  抖音创作助手登录入口_抖音创作辅助工具官网直达  微信群消息显示延迟如何解决 微信群消息刷新优化方法 

搜索