React函数式组件中日历渲染的正确时机与副作用管理

React函数式组件中日历渲染的正确时机与副作用管理

本教程旨在解决React函数式组件中因DOM操作时机不当导致的渲染问题。我们将深入探讨如何利用useState管理组件状态,通过useEffect在组件挂载后安全执行副作用操作(如日历渲染),并使用useCallback优化函数性能。此外,还将介绍条件渲染以确保DOM元素存在,并强调React中避免直接DOM操作的最佳实践。

1. 理解React组件生命周期与副作用

在react函数式组件中,直接在组件主体(函数顶层)调用会修改dom或依赖dom存在的函数,通常会导致不可预测的行为,例如在组件初次渲染时无法正常工作,或者在数据更新后出现问题。这是因为react组件的渲染过程是声明式的,它首先根据状态和属性构建虚拟dom,然后将虚拟dom与真实dom进行比较并更新。而像document.queryselector这样的命令式dom操作,如果其目标元素在组件渲染完成前尚未存在于真实dom中,则会失败。

原始代码中,renderCalendar()在组件函数体内被直接调用,此时组件的JSX尚未完全渲染到真实DOM中,导致currentDate和daysTag等通过document.querySelector获取的元素可能为空,从而使日历无法正确初始化。即使通过某种方式使其工作,刷新页面后问题依然存在,这进一步证实了时序问题。

为了解决这类问题,React提供了特定的钩子(Hooks)来管理副作用,确保代码在正确的时机执行。

2. 核心解决方案:useState与useEffect

要确保renderCalendar函数在DOM元素可用时才执行,并仅在必要时重新执行,我们需要结合使用useState、useEffect和useCallback。

2.1 管理渲染状态:useState

首先,引入一个状态变量来标记日历是否已经成功渲染。这对于后续的条件渲染和确保renderCalendar只在DOM准备好后执行至关重要。

import React, { useState, useEffect, useCallback } from 'react';// ... 其他导入export const Calendar = () => {  // ... 其他变量定义  // 添加一个状态来追踪日历是否已渲染  const [renderedCalendar, setRenderedCalendar] = useState(false);  // ... rest of the component};

2.2 确保DOM就绪后执行:useEffect

useEffect是React中处理副作用的钩子。它会在组件渲染到DOM之后执行,是执行DOM操作、数据获取、订阅事件等副作用的理想场所。我们将renderCalendar的调用放入useEffect中,以确保它在组件的JSX内容(包括.current-date和.days元素)被渲染到DOM之后才尝试访问它们。

import React, { useState, useEffect, useCallback } from 'react';import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';export const Calendar = () => {  // 注意:此处获取DOM元素的代码应移动到renderCalendar内部,  // 并且在React中通常推荐使用ref或通过状态管理数据来避免直接DOM操作。  // 但为了解决原问题,暂时保留其位置,但在renderCalendar内部确保获取成功。  let date = new Date();  let currYear = date.getFullYear();  let currMonth = date.getMonth();  const months = ["January", "February", "March", "April",                  "May", "June","July", "August", "September",                  "October", "November", "December"];  const [renderedCalendar, setRenderedCalendar] = useState(false);  // 使用useCallback包装renderCalendar以优化性能,防止不必要的重新创建  const renderCalendar = useCallback(() => {    // 确保在函数执行时获取到DOM元素    const currentDateElement = document.querySelector(".current-date");    const daysTagElement = document.querySelector(".days");    if (!currentDateElement || !daysTagElement) {      // 如果元素尚未就绪,则不执行渲染逻辑,或者可以抛出错误/记录警告      console.warn("Calendar elements not found in DOM yet.");      return;    }    let firstDayofMonth = new Date(currYear, currMonth, 1).getDay();    let lastDateofMonth = new Date(currYear, currMonth + 1, 0).getDate();    let lastDayofMonth = new Date(currYear, currMonth, lastDateofMonth).getDay();    let lastDateofLastMonth = new Date(currYear, currMonth, 0).getDate();    let liTag= "";    for (let i = firstDayofMonth; i > 0; i--) {      liTag += `
  • ${lastDateofLastMonth - i + 1}
  • `; }; for(let i = 1; i<= lastDateofMonth; i++) { let isToday = i === new Date().getDate() && currMonth === new Date().getMonth() && currYear === new Date().getFullYear() ? "active" : ""; liTag += `
  • ${i}
  • `; }; for(let i = lastDayofMonth; i<= 5; i++) { liTag += `
  • ${i - lastDayofMonth + 1}
  • `; }; currentDateElement.innerText = `${months[currMonth]} ${currYear}`; daysTagElement.innerHTML = liTag; // 日历渲染完成后,更新状态 setRenderedCalendar(true); }, [currYear, currMonth]); // 依赖项:当currYear或currMonth变化时,renderCalendar函数才需要重新创建 // 使用useEffect在组件挂载后调用renderCalendar useEffect(() => { renderCalendar(); }, [renderCalendar]); // 依赖项:当renderCalendar函数本身变化时,重新执行 // ... rest of the component};

    依赖数组的理解:

    renderCalendar函数的useCallback依赖数组[currYear, currMonth]意味着,只有当currYear或currMonth的值发生变化时,renderCalendar函数才会重新创建。这有助于优化性能。useEffect的依赖数组[renderCalendar]意味着,只有当renderCalendar函数本身发生变化时(即currYear或currMonth变化导致它重新创建),useEffect才会重新执行。这样确保了在日历月份或年份变化时,renderCalendar会被再次调用以更新显示。

    2.3 处理事件监听器

    原始代码中的事件监听器也存在类似的问题,它们在组件渲染时被直接添加,但prevNextIcon同样依赖于DOM元素的存在。更重要的是,在React中,通常不直接使用addEventListener。我们应该使用React的事件系统。

      // 移除原始的forEach和addEventListener  // prevNextIcon.forEach ( icon => { ... });  const handleArrowClick = (id) => {    currMonth = id === "prev" ? currMonth - 1 : currMonth + 1;    if(currMonth  11) {      date = new Date(currYear, currMonth);      currYear = date.getFullYear();      currMonth = date.getMonth();    }    else {      date = new Date(); // 这行可能需要根据实际需求调整,通常是保持当前日期或重置为新月份的第一天    };    renderCalendar(); // 每次月份改变时重新渲染日历  };

    注意: currMonth和currYear在函数组件中应该作为状态来管理,而不是普通变量。否则,它们的改变不会触发组件重新渲染,导致renderCalendar虽然执行了,但JSX没有更新。

    3. 优化与重构:React最佳实践

    原始代码中直接操作DOM(document.querySelector, innerText, innerHTML)是React中应尽量避免的模式。React推崇声明式UI,即通过管理组件的状态来让React自动更新DOM。

    3.1 使用React State管理日历数据

    将currYear、currMonth以及日历中的天数数据存储在React的状态中。当这些状态改变时,React会自动重新渲染组件。

    import React, { useState, useEffect, useCallback, useMemo } from 'react';import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';export const Calendar = () => {  const [currentDate, setCurrentDate] = useState(new Date());  const [currYear, setCurrYear] = useState(currentDate.getFullYear());  const [currMonth, setCurrMonth] = useState(currentDate.getMonth());  const months = ["January", "February", "March", "April",                  "May", "June","July", "August", "September",                  "October", "November", "December"];  // 使用useMemo来计算日历天数,只有在currYear或currMonth变化时才重新计算  const calendarDays = useMemo(() => {    let liTag = [];    let firstDayofMonth = new Date(currYear, currMonth, 1).getDay();    let lastDateofMonth = new Date(currYear, currMonth + 1, 0).getDate();    let lastDateofLastMonth = new Date(currYear, currMonth, 0).getDate();    // 上个月的日期    for (let i = firstDayofMonth; i > 0; i--) {      liTag.push({        date: lastDateofLastMonth - i + 1,        className: "inactive"      });    };    // 当前月的日期    for(let i = 1; i<= lastDateofMonth; i++) {      let isToday = i === new Date().getDate() && currMonth === new Date().getMonth()                    &&  currYear === new Date().getFullYear() ? "active" : "";      liTag.push({        date: i,        className: isToday      });    };    // 下个月的日期    for(let i = lastDayofMonth; i {    let newMonth = id === "prev" ? currMonth - 1 : currMonth + 1;    let newYear = currYear;    if(newMonth  11) {      const newDate = new Date(currYear, newMonth);      newYear = newDate.getFullYear();      newMonth = newDate.getMonth();    }    setCurrMonth(newMonth);    setCurrYear(newYear);  }, [currMonth, currYear]);  // 注意:现在renderCalendar函数不再需要,因为我们直接在JSX中渲染calendarDays  // useEffect(() => {  //   // 可以在这里执行一些依赖于DOM但与日历内容无关的副作用,例如初始化第三方库  // }, []);  return (    

    Calendar

    {months[currMonth]} {currYear}

    handleArrowClick("prev")}/> handleArrowClick("next")}/>
    • Sunday
    • Monday
    • Tuesday
    • Wednesday
    • Thursday
    • Friday
    • Saturday
      {calendarDays.map((day, index) => (
    • {day.date}
    • ))}
    );}

    3.2 最终的优化与注意事项

    避免document.querySelector: 除非与不受React控制的第三方DOM库交互,否则应避免直接使用document.querySelector来操作React组件内部的DOM。React通过状态和属性来管理UI更新。使用useState管理可变数据: 任何会随时间变化并影响组件渲染的数据都应该存储在useState中。useEffect用于副作用: 当你需要执行与组件渲染无关的操作(如数据获取、订阅、手动DOM操作等)时,使用useEffect。确保正确设置依赖数组,以避免不必要的重复执行或遗漏执行。useCallback和useMemo优化性能: 当函数或计算结果作为useEffect的依赖项或传递给子组件时,使用useCallback和useMemo可以防止它们在每次渲染时都被重新创建,从而提高性能。事件处理: 使用React的合成事件系统(如onClick)而不是addEventListener。

    通过上述重构,我们不仅解决了日历初次渲染的问题,还将组件转换为了更符合React范式的声明式组件。这使得代码更易于理解、维护和扩展。

    以上就是React函数式组件中日历渲染的正确时机与副作用管理的详细内容,更多请关注创想鸟其它相关文章!

    版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
    如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
    发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1509701.html

    (0)
    打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
    上一篇 2025年12月20日 05:34:55
    下一篇 2025年12月20日 05:35:09

    相关推荐

    • Angular中“加载更多”按钮延迟隐藏问题的深度解析与优化

      在Angular应用中实现“加载更多”功能时,如果“加载更多”按钮的隐藏逻辑处理不当,可能导致按钮在所有数据加载完毕后仍需额外点击一次才能消失。本文将深入分析这一常见问题,揭示其根本原因在于状态更新与条件判断的顺序,并提供一个可靠的解决方案,通过调整loadMore函数的内部逻辑,确保按钮在数据完全…

      2025年12月20日
      000
    • 创建滚动时固定在容器顶部的侧边栏

      本文旨在解决在网页开发中创建滚动时固定在容器顶部的侧边栏的问题。我们将提供详细的代码示例和解释,帮助开发者实现一个在指定容器内保持置顶的侧边栏效果,并避免与其他内容发生重叠。通过本文的学习,你将掌握利用 JavaScript 和 CSS 实现粘性侧边栏的关键技术。 实现粘性侧边栏 在网页设计中,粘性…

      2025年12月20日
      000
    • Angular应用中“加载更多”按钮延迟隐藏问题的精确控制与解决方案

      本教程深入探讨了Angular应用中“加载更多”按钮在所有数据加载完毕后仍延迟隐藏的问题。通过分析初始逻辑的缺陷,我们提供了一种优化loadMore函数的解决方案,确保itemsNumber更新后立即准确评估按钮的可见性,从而实现更流畅的用户体验和精确的UI状态管理。 1. 问题背景与现象分析 在开…

      2025年12月20日
      000
    • 实现滚动吸顶效果:让Aside元素在容器内保持可见

      实现滚动吸顶效果:让Aside元素在容器内保持可见 本文旨在提供一种实现滚动吸顶效果的方案,使aside元素在容器内滚动时保持在顶部,直到容器底部。通过监听滚动事件并动态修改元素的position属性,可以实现这一效果。本文将详细介绍实现原理、代码示例以及注意事项,帮助开发者轻松实现滚动吸顶功能。 …

      2025年12月20日
      000
    • GeoJSON多边形坐标有效性验证指南

      本文旨在解决在使用Mapbox等地图库绘制多边形时,因GeoJSON数据无效而导致的错误。我们将介绍如何利用Turf.js库中的@turf/boolean-valid模块,在绘制多边形之前对其坐标有效性进行预验证,从而确保数据符合GeoJSON规范,避免运行时错误,提升应用的健壮性。 1. GeoJ…

      2025年12月20日
      000
    • GeoJSON多边形有效性校验:使用Turf.js避免绘图错误

      在使用Mapbox等地图库绘制GeoJSON多边形时,常因数据无效导致错误。本文将介绍如何利用Turf.js库中的booleanValid函数,在绘图前高效校验多边形坐标的有效性,确保GeoJSON数据的合规性,从而避免运行时错误,提升应用稳定性。通过示例代码和注意事项,帮助开发者正确处理多边形数据…

      2025年12月20日
      000
    • JavaScript 实现动态背景颜色切换:无需刷新页面

      本文旨在解决网页背景颜色动态切换的问题,避免每次点击按钮都需要刷新页面的困扰。我们将通过 JavaScript 代码,实现点击按钮后随机改变背景颜色,并将颜色名称显示在页面上。核心在于每次点击事件发生时,都重新生成随机颜色值,确保每次都能获得新的颜色。 实现动态背景颜色切换 要实现点击按钮动态改变网…

      2025年12月20日
      000
    • 动态改变网页背景颜色:无需重新加载页面

      本文旨在解决网页背景颜色动态切换的问题,避免每次点击按钮都需要重新加载页面的情况。通过将颜色随机选择逻辑放置于按钮点击事件处理函数内部,确保每次点击都能获取新的颜色值,从而实现背景颜色的动态更新。本文将提供详细的代码示例和解释,帮助开发者轻松实现此功能。 在网页开发中,动态改变页面元素(例如背景颜色…

      2025年12月20日
      000
    • JavaScript 实现点击按钮动态更改背景颜色

      本文旨在帮助开发者解决使用 JavaScript 点击按钮动态更改网页背景颜色时,颜色仅改变一次的问题。通过将随机颜色选择逻辑置于按钮点击事件处理函数内部,确保每次点击都能生成新的颜色值,从而实现动态颜色切换的效果。文章提供详细的代码示例和解释,帮助读者理解并掌握该技巧。 在网页开发中,经常需要通过…

      2025年12月20日
      000
    • 动态修改网页背景颜色:无需刷新页面的实现方法

      本文旨在指导开发者如何在不刷新页面的情况下,通过点击按钮动态改变网页的背景颜色。我们将通过 JavaScript 实现颜色数组的随机选取,并将其应用到网页背景上,从而解决每次点击后需要刷新才能更新颜色的问题。 实现原理 核心问题在于,原代码只在页面加载时执行一次随机颜色选择,并将结果存储在 item…

      2025年12月20日
      000
    • 使用 Flask 在客户端动态构建内容:一个教程

      在 Flask 应用中,我们经常需要在服务器端动态生成内容,并将其展示在客户端。本文将探讨一种有效的方法,即利用 Flask 的路由机制和 HTML5 的 标签,实现音频内容的动态生成和自动播放。这种方法避免了直接操作客户端文件系统,简化了开发流程。 问题背景 最初的尝试是在 Flask 应用中使用…

      2025年12月20日
      000
    • 使用 Flask 动态构建客户端内容:一种正确的实现方式

      第一段引用上面的摘要: 本文旨在帮助开发者理解如何使用 Flask 框架在服务器端动态生成内容,并将其有效地传递到客户端进行展示,同时保持客户端的交互性。文章将剖析一个常见的错误尝试,并提供一个基于Response对象和url_for函数的正确解决方案,以实现音频文件的动态生成和播放,并兼顾客户端页…

      2025年12月20日
      000
    • Flask 应用中动态生成并流式传输客户端音频教程

      本教程详细探讨了在 Flask 应用中如何动态生成音频文件并将其流式传输到客户端,同时保持用户在当前 HTML 页面上的焦点。文章纠正了在视图函数中使用 app.post 的错误方法,并提供了基于 Flask 路由和 HTML5 标签的正确解决方案,展示了如何通过将音频流作为响应返回,并在前端通过 …

      2025年12月20日
      000
    • 使用 Flask 动态构建客户端内容:一种基于音频播放的教程

      本文档旨在指导开发者如何使用 Flask 框架,通过服务端动态生成音频内容,并在客户端页面上自动播放。我们将探讨如何利用 Flask 的路由和模板引擎,结合 HTML5 的 标签,实现服务端生成音频并无缝集成到客户端页面的功能,同时保持用户与页面的交互体验。 问题背景 在 Flask 应用中,有时我…

      2025年12月20日
      000
    • 动态修改网页背景颜色:无需刷新页面的JavaScript实现

      本文旨在提供一种使用JavaScript动态修改网页背景颜色的方法,无需每次点击按钮都刷新页面。通过将颜色随机选择逻辑置于点击事件处理函数内部,确保每次点击都能获取新的颜色值,从而实现动态背景颜色切换。本文将提供详细代码示例和注意事项,帮助开发者轻松实现这一功能。 要实现点击按钮动态改变网页背景颜色…

      2025年12月20日
      000
    • 在Angular中基于另一JSON筛选数据:实用教程

      本文详细介绍了如何在Angular(JavaScript环境)中,高效地根据一个JSON数组(包含筛选ID)来过滤另一个大型JSON数组(包含完整记录)。通过结合使用JavaScript的Array.prototype.filter()和Array.prototype.some()方法,可以实现精确…

      2025年12月20日
      000
    • 使用 Angular 过滤 JSON 数据:根据 ID 匹配筛选

      本文介绍了在 Angular 项目中,如何利用 JavaScript 的 Array.prototype.filter() 和 Array.prototype.some() 方法,根据一个 JSON 数组(包含 ID)来过滤另一个 JSON 数组,从而提取出匹配 ID 的记录。通过本文提供的示例代码…

      2025年12月20日
      000
    • 在Angular中高效筛选JSON数据:基于ID匹配实现

      本文详细介绍了如何在Angular(JavaScript环境)中,根据一个JSON数组中包含的ID列表,高效地从另一个包含完整记录的JSON数组中筛选出匹配的数据。核心解决方案是利用JavaScript原生的Array.prototype.filter()和Array.prototype.some(…

      2025年12月20日
      000
    • 在Angular中根据ID高效筛选JSON数据

      本文详细介绍了如何在Angular应用中,利用JavaScript的Array.prototype.filter()和Array.prototype.some()方法,根据一个JSON数组中的ID列表,从另一个包含完整记录的JSON数组中筛选出匹配的数据。通过具体的代码示例和解析,读者将掌握一种简洁…

      2025年12月20日
      000
    • 在Angular中基于另一JSON筛选数据记录的实用教程

      本教程详细介绍了如何在Angular(或任何JavaScript环境)中,高效地根据一个JSON数组中的ID,筛选出另一个包含完整数据记录的JSON数组。核心方法是结合使用JavaScript的Array.prototype.filter()和Array.prototype.some(),通过示例代…

      2025年12月20日
      000

    发表回复

    登录后才能评论
    关注微信