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


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父组件。

Listnr Listnr

AI文本到语音生成器

Listnr 180 查看详情 Listnr

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中子组件向父组件传递状态:实现倒计时结束通知的详细内容,更多请关注其它相关文章!


# js  # react  # 单选  # 已到  # 第二个  # 这是  # 倒计时  # red  # 应用开发  # ios  # ai  # axios  # go  # 旗袍微博营销推广文案  # 小餐车全网推广营销策略  # 网站建设相关工作安排  # 深圳营销推广商场  # 西藏seo优化有效果吗  # 互联网项目营销推广  # 宝鸡网站优化宝鸡网站  # 推广营销媒体  # 西乡网站的建设  # 新民媒体网站建设方案  # 零时  # 多个  # 第一个  # 这一  # 自己的 


相关栏目: 【 Google疑问12 】 【 Facebook疑问10 】 【 优化推广96088 】 【 技术知识133117 】 【 IDC资讯59369 】 【 网络运营7196 】 【 IT资讯61894


相关推荐: 苹果iPhone14ProMax如何新建AppleID_iPhone14ProMax新建AppleID具体流程  《百果园》充值余额方法  windows10怎么关闭自动安装应用_windows10禁止推广应用下载  漫蛙manwa漫画官网链接_漫蛙manwa最新可用网址推荐  QQ邮箱手机版网页版 QQ邮箱登录入口地址  PSD转AI文件的简单方法  OTT月报 | 2025年9月智能电视大数据报告  《深林》冬季章节图文攻略  国际经济与贸易就业方向解析  t3出行如何使用微信支付  优化 WooCommerce 产品价格显示与自定义短代码集成  优酷下载视频的清晰度怎么选_优酷缓存清晰度设置与选择指南  基于键值条件高效映射 Pandas DataFrame 多列数据  优化CSS动画与J*aScript定时器协同:构建稳定Toast提示  人教版电子教材在线获取指南  word文档中的分隔符有哪些不同类型和用途_Word分隔符类型与用途方法  邦丰播放器频道搜索设置  荣耀Magic6 Pro拍照成像偏暗_荣耀Magic6 Pro夜景优化  教资成绩怎么查询  《虎扑》关闭社区内容推荐方法  excel怎么计算平均值 excel平均函数*ERAGE使用教学  《KARDS》冬季扩展包“国土阵线”上线!全新“协力”机制改变战场格局  win11怎么更改账户类型 Win11标准用户和管理员权限切换【教程】  我的世界游戏平台入口 我的世界官方官网直达链接  J*aScript桌面应用_Electron多进程架构实战  Golang如何使用log记录日志信息_Golang log日志记录方法总结  热血江湖归来医师加点攻略  智学网app怎么登录忘记密码_智学网app忘记密码找回与重新登录操作方法  微信客户端怎么查看二维码_微信客户端个人二维码查看方法  Keras中Convolution2D层及其核心辅助层详解  mysql如何管理数据库账户_mysql数据库账户管理技巧  《荔枝fm》导出文件教程  《爱南宁》认证电动车方法  德邦快递收费标准详解  Python测试中模块导入路径解析的最佳实践  《合金装备4》有望推出重制版!制作人发话了  vivo浏览器怎么离线保存网页 vivo浏览器下载完整页面以便无网络时阅读  composer 提示 "requires ext-soap" 缺少 SOAP 扩展怎么办?  多闪APP官方下载安装入口_多闪最新版本获取入口  Linux如何自动分析系统异常日志_Linux日志智能检测  《下一站江湖2》独孤剑诀习得方法  网页版网易云音乐入口_网易云音乐在线官网登录  Windows 11怎么删除恢复分区_Windows 11使用Diskpart命令强行删除分区  VS Code快捷键when上下文子句的妙用  《土豆雅思》修改密码方法  在VS Code中进行数据科学和机器学习开发  如何测试您的网站全球打开速度-网站海外测速工  lol小红书怎么|直播|?lol小红书|直播|是什么意思?  Flask 应用中图片动态更新与上传:实现客户端定时刷新与服务器端文件管理  猫眼电影app如何参与官方的抽奖活动_猫眼电影官方抽奖参与方法 

 2025-12-13

了解您产品搜索量及市场趋势,制定营销计划

同行竞争及网站分析保障您的广告效果

点击免费数据支持

提交您的需求,1小时内享受我们的专业解答。

运城市盐湖区信雨科技有限公司


运城市盐湖区信雨科技有限公司

运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。

 8156699

 13765294890

 8156699@qq.com

Notice

We and selected third parties use cookies or similar technologies for technical purposes and, with your consent, for other purposes as specified in the cookie policy.
You can consent to the use of such technologies by closing this notice, by interacting with any link or button outside of this notice or by continuing to browse otherwise.