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


相关推荐: 《随手记》关闭首页消息推送方法  哔哩哔哩黑名单怎么查看  中大网校app做题记录清除方法  Python实时数据流中高效查找最大最小值  《撕歌》会员开通方法  如何使用CSS Grid实现“大方块左侧,小方块右侧垂直堆叠”的水平布局  《顺丰同城骑士》查看我的技能方法  《领英》查看屏蔽名单方法  抖音小程序怎么开通?小程序开通条件是什么?  VS Code的时间线(Timeline)视图:您的代码时光机  iPhone14开启Apple TV遥控设置  百度竞价WAP显示PC链接问题  PSD转AI文件的简单方法  拷贝漫画2025网页版入口 拷贝漫画官网免费看全集  LocoySpider如何批量采集电商商品_LocoySpider电商采集的模板应用  Golang如何使用log记录日志信息_Golang log日志记录方法总结  淘口令快速解析技巧  多闪电脑版下载_多闪PC端模拟器使用  雨课堂官网在线登录 网页版雨课堂登录链接  天天漫画2025最新入口 天天漫画永久有效登录入口  iPhone12是否要更新ios16  德邦快递收费标准详解  如何在Python中安全地将环境变量转换为整数并满足Mypy类型检查  《浙里办》电子发票开具方法  Go Template中优雅处理循环最后一项:自定义函数实践  React应用中Commerce.js数据加载与状态管理最佳实践  C#解析来自网络的XML流数据 实时错误处理与重试机制  win11如何开启单声道音频 Win11为听障用户合并左右声道【辅助】  QQ邮箱手机版网页版 QQ邮箱登录入口地址  微信如何设置字体大小_微信字体设置的阅读舒适  《幻兽帕鲁》手游帕鲁捕捉技巧分享  Flexbox布局实践:实现底部页脚与顶部粘性导航条的完美结合  J*aScript 数值去小数位处理:多种方法与实践  iPhone 13 Pro Max如何设置桌面小组件_iPhone 13 Pro Max小组件添加指南  sublime text 4如何安装_最新版sublime下载与汉化教程  解决异步Python机器人中同步操作的阻塞问题  Selenium自动化:利用键盘模拟解决复杂日期输入框输入问题  MongoDB聚合管道:高效统计列表中各项的文档数量  网易云音乐闹钟铃声设置教程  研招网官方网站招生平台入口_中国研究生招生信息网官网登录  我居然低估了 DeepSeek,这次更新它做到了这些!  优化Google Charts Gauge:在数据库无数据时显示默认值  《异星探险家》古怪的物品作用介绍  TikTok网页版入口快速访问 TikTok官网账号登录方法  Python对象引用与属性赋值:理解链表中的行为  如何在mysql中设计餐饮点餐系统_mysql点餐系统项目实战  Lar*el 关联查询:同时筛选父表与子表数据的高效策略  Win10如何关闭开机锁屏界面_Windows10跳过锁屏直接登录设置  《荔枝fm》导出文件教程  word怎么将图片设置为页面背景并不影响打印_Word图片背景设置方法 

 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.