React 函数组件日历渲染:告别 DOM 操作,拥抱状态驱动

react 函数组件日历渲染:告别 dom 操作,拥抱状态驱动

本教程深入探讨了 React 函数组件中日历渲染的常见问题,特别是避免直接 DOM 操作(如 document.querySelector 和 innerHTML)。我们将详细阐述如何利用 React 的核心机制——状态管理 (useState) 和副作用钩子 (useEffect)——来构建一个完全由数据驱动的、高效且易于维护的日历组件,确保其在首次加载和用户交互时都能无缝且正确地更新视图。

引言:React 中的 DOM 操作原则

React 的核心理念是声明式 UI。这意味着开发者通过描述 UI 的“状态”来构建界面,而不是直接操作 DOM 元素。React 会根据状态的变化自动、高效地更新 DOM。这种模式极大地简化了 UI 开发,并提高了应用的可维护性。

然而,当我们在 React 组件中直接使用诸如 document.querySelector、innerHTML 或 addEventListener 等原生 DOM API 时,就违背了 React 的声明式原则。这可能导致以下问题:

渲染不一致性: React 维护一个虚拟 DOM 来优化更新。直接操作真实 DOM 会绕过虚拟 DOM,导致 React 无法感知这些变化,进而引发视图与内部状态不一致的问题。性能问题: React 有一套高效的协调(Reconciliation)算法来最小化 DOM 操作。直接操作 DOM 会破坏这种优化。难以调试和维护: 逻辑分散在 React 的生命周期和原生 DOM 操作中,使得问题追踪变得复杂。兼容性与可移植性差: 代码与特定 DOM 环境耦合,难以在不同平台(如 React Native)复用。

原始问题中的日历组件正是因为混合了命令式 DOM 操作与 React 组件模式,才导致了初始化渲染的困扰和后续难以维护的局面。

原问题分析与常见误区

在原始代码中,renderCalendar() 函数内部使用了 document.querySelector 来获取 DOM 元素,并使用 innerText 和 innerHTML 直接修改它们。

export const Calendar = () => {  const currentDate = document.querySelector(".current-date") || {}; // 问题点1  let daysTag = document.querySelector(".days") || {};               // 问题点2  // ...  const renderCalendar = () => {    // ...    currentDate.innerText = `${months[currMonth]} ${currYear}`; // 问题点3    daysTag.innerHTML = liTag;                                  // 问题点4  };  renderCalendar(); // 首次调用位置  // ...  prevNextIcon.forEach ( icon => {    icon.addEventListener("click", () => { // 问题点5      // ...      renderCalendar();    });  });  // ...  return (    // ... JSX 结构  );}

分析原代码中 renderCalendar() 的调用问题:

在 return 语句前调用 renderCalendar():

当 React 组件首次渲染时,其函数体从上到下执行。在 return 语句执行并返回 JSX 结构之前,对应的真实 DOM 元素(如 .current-date 和 .days)尚未被创建并挂载到文档中。因此,document.querySelector(“.current-date”) 和 document.querySelector(“.days”) 在此时会返回 null。尽管代码中使用了 || {} 来避免运行时错误,但 currentDate 和 daysTag 实际上是空对象,后续对它们的 innerText 或 innerHTML 赋值将无效。这就是为什么首次加载时日历无法正确显示,而刷新后又可能暂时显示的原因(因为浏览器缓存或某种时序巧合)。

在 return 语句后调用 renderCalendar():

JavaScript 函数的执行流程决定,一旦遇到 return 语句,函数就会立即终止并返回结果。任何在 return 语句之后的代码都将是“不可达代码”(Unreachable code),永远不会被执行。终端中收到的“Unreachable code”警告正是对此的提示。

React 最佳实践:状态驱动与声明式渲染

解决上述问题的核心在于拥抱 React 的状态驱动和声明式渲染范式。我们将通过 useState 和 useEffect 钩子来重构日历组件。

useState:管理组件状态

所有影响组件渲染的数据(如当前的年份、月份、日历中的日期列表、当前日期文本等)都应该存储在 useState 中。当这些状态改变时,React 会自动重新渲染组件。

useEffect:处理副作用

useEffect 钩子用于处理组件渲染后需要执行的“副作用”,例如数据获取、订阅事件、或基于状态变化进行复杂计算。我们可以将计算日历日期列表的逻辑放入 useEffect 中。通过指定依赖项(如 currYear 和 currMonth),我们可以确保当年份或月份变化时,日历数据会重新计算。

JSX:声明式渲染 UI

不再使用 innerHTML 或 innerText 直接操作 DOM。而是通过 JSX 语法,将状态数据直接绑定到 UI 元素上。例如,日期列表可以通过映射一个日期数据数组来动态生成 元素。

重构日历组件

我们将逐步重构 Calendar 组件,使其完全符合 React 的开发范式。

步骤一:定义状态

首先,我们需要定义组件所需的各种状态:当前的年份、月份、日历显示的日期文本,以及日历中所有日期(包括上月、本月、下月)的列表。

import React, { useState, useEffect, useCallback } from 'react';import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';export const Calendar = () => {  // 初始化当前日期  const today = new Date();  const [currYear, setCurrYear] = useState(today.getFullYear());  const [currMonth, setCurrMonth] = useState(today.getMonth()); // 0-11  const [currentDateText, setCurrentDateText] = useState(""); // 用于显示 "May 2023"  const [daysInMonth, setDaysInMonth] = useState([]); // 用于存储日历中的所有日期数据  const months = [    "January", "February", "March", "April", "May", "June",    "July", "August", "September", "October", "November", "December"  ];  // ... (其他代码)};

步骤二:封装计算逻辑到 useEffect

将原 renderCalendar 的逻辑重写为一个计算函数,该函数不再直接操作 DOM,而是计算出日期数据并更新状态。然后,在 useEffect 中调用这个计算函数。

  // 使用 useCallback 优化,避免每次渲染都重新创建函数  const generateCalendarDays = useCallback(() => {    let firstDayofMonth = new Date(currYear, currMonth, 1).getDay(); // 获取当月第一天是星期几 (0-6)    let lastDateofMonth = new Date(currYear, currMonth + 1, 0).getDate(); // 获取当月最后一天    let lastDayofLastMonth = new Date(currYear, currMonth, 0).getDate(); // 获取上月最后一天    let days = [];    // 填充上月日期    for (let i = firstDayofMonth; i > 0; i--) {      days.push({        date: lastDayofLastMonth - i + 1,        type: 'inactive',        key: `prev-${lastDayofLastMonth - i + 1}` // 添加唯一 key      });    }    // 填充当月日期    for (let i = 1; i <= lastDateofMonth; i++) {      let isToday = i === today.getDate() && currMonth === today.getMonth() && currYear === today.getFullYear();      days.push({        date: i,        type: isToday ? 'active' : '',        key: `curr-${i}`      });    }    // 填充下月日期 (确保日历有 6 行,即 42 个格子)    let totalDaysDisplayed = days.length;    let remainingSlots = 42 - totalDaysDisplayed; // 假设日历固定显示 6 周    if (remainingSlots < 0) remainingSlots = 0; // 避免负数,虽然通常不会发生    for (let i = 1; i  {    generateCalendarDays();  }, [generateCalendarDays]); // 依赖 generateCalendarDays 函数

步骤三:JSX 声明式渲染

现在,我们可以利用 currentDateText 和 daysInMonth 这两个状态来渲染 UI。

  return (    

Calendar

{currentDateText}

{/* 直接绑定状态 */}
{ setCurrMonth(prevMonth => { let newMonth = prevMonth - 1; if (newMonth prevYear - 1); return 11; // 11代表12月 } return newMonth; }); }}/> { setCurrMonth(prevMonth => { let newMonth = prevMonth + 1; if (newMonth > 11) { setCurrYear(prevYear => prevYear + 1); return 0; // 0代表1月 } return newMonth; }); }}/>
  • Sunday
  • Monday
  • Tuesday
  • Wednesday
  • Thursday
  • Friday
  • Saturday
    {daysInMonth.map(day => (
  • {day.date}
  • // 映射数组生成列表 ))}
);

完整的重构代码示例

import React, { useState, useEffect, useCallback } from 'react';import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';export const Calendar = () => {  // 获取当前日期,用于标记“今天”  const today = new Date();  // 使用 useState 管理当前显示的年份和月份  const [currYear, setCurrYear] = useState(today.getFullYear());  const [currMonth, setCurrMonth] = useState(today.getMonth()); // 月份从0开始 (0-11)  // 用于显示日历

以上就是React 函数组件日历渲染:告别 DOM 操作,拥抱状态驱动的详细内容,更多请关注创想鸟其它相关文章!

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

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月20日 05:32:14
下一篇 2025年12月20日 05:32:21

相关推荐

  • React 函数组件中DOM操作与副作用管理:构建可靠日历组件的实践

    本文旨在解决React函数组件中不当的DOM操作和副作用管理导致的组件初始化问题。通过深入探讨useEffect、useState和useCallback等React Hooks,我们将展示如何正确地在组件挂载和更新时执行逻辑,避免直接操作DOM的常见陷阱,并构建一个稳定、可预测的日历组件。文章将提…

    2025年12月20日
    000
  • JavaScript的appendChild方法是什么?怎么用?

    appendchild是javascript中用于向父节点末尾添加新子节点的方法,它返回被添加的节点。其基本用法是通过获取父元素、创建新元素、调用appendchild将新元素添加到父元素中;当参数为已存在节点时,会将其从原位置移动到新位置。与insertbefore不同,appendchild始终…

    2025年12月20日 好文分享
    000
  • Shopify:在集合列表中访问和筛选产品

    本文将深入探讨在 Shopify Liquid 模板中访问集合产品并进行筛选的技巧。正如摘要所述,我们将重点解决 Shopify 集合分页限制带来的问题,并介绍如何利用 where 过滤器,基于产品属性(如供应商)进行高效筛选。掌握这些方法,你将能够更精确地控制集合中产品的展示,从而显著提升店铺的用…

    2025年12月20日
    000
  • JavaScript如何用Promise.all处理多个异步

    promise.all用于处理多个异步操作,接收一个promise数组并在所有promise都resolve后返回结果数组;若任一promise reject,则立即返回该错误。1. promise.all适用于需所有异步操作均成功完成的场景,如并行请求多个api、加载多个资源、执行多个数据库查询等…

    2025年12月20日 好文分享
    000
  • BOM中如何操作浏览器的下载功能?

    前端无法直接控制浏览器下载细节,但可通过html的标签触发下载。1. 使用标签并设置download属性,指定文件名和href链接,可下载服务器文件;2. 利用blob对象封装数据,结合url.createobjecturl生成临时链接,实现客户端生成文件并下载;3. 避免使用window.open…

    2025年12月20日 好文分享
    000
  • JavaScript中微任务与宏任务区别

    javascript中微任务优先于宏任务执行。事件循环先执行宏任务,完成后清空微任务队列,再进入下一宏任务。常见宏任务包括整体脚本、settimeout回调、i/o操作、ui渲染等;常见微任务包括promise回调、mutationobserver、queuemicrotask。理解两者执行顺序可避…

    2025年12月20日 好文分享
    000
  • 如何用JavaScript操作HTML5的Vibration API?

    javascript操作html5的vibration api核心是调用navigator.vibrate()方法,可传入数字或数组定义振动时长或复杂模式;例如navigator.vibrate(500)实现500毫秒振动,navigator.vibrate([200, 100, 400, 100,…

    2025年12月20日 好文分享
    000
  • 如何处理异步函数的依赖关系

    处理异步函数依赖关系的核心在于确保操作顺序性与协调性,1.通过promise实现基础链式调用,明确任务顺序执行;2.使用async/await提升代码可读性与维护性,避免回调地狱;3.promise.all()用于并行执行多个独立任务并等待全部完成;4.promise.race()用于获取最先完成的…

    2025年12月20日 好文分享
    000
  • BOM中如何操作浏览器的画中画功能?

    操作浏览器画中画功能的核心在于使用htmlvideoelement的requestpictureinpicture()方法进入pip模式,以及document.exitpictureinpicture()退出;1. 进入pip需调用videoelement.requestpictureinpictu…

    2025年12月20日 好文分享
    000
  • JavaScript DOM操作:获取并插入指定元素的内部HTML内容

    本文详细介绍了如何使用JavaScript的DOM操作来获取特定HTML元素的内部HTML内容,并将其动态插入到另一个指定元素中。文章通过分析常见错误,阐明了document.getElementById()方法的正确用法和innerHTML属性的应用,并提供了清晰的步骤和完整的代码示例,旨在帮助开…

    2025年12月20日
    000
  • Shopify教程:在集合列表中筛选和展示特定产品

    本文旨在解决Shopify Liquid模板中,从集合列表中筛选并展示特定产品的问题。通过分析常见问题和提供示例代码,帮助开发者有效地根据产品标题或其他属性筛选产品,并解决因分页限制导致的产品显示不完整的问题。本文将重点介绍where过滤器,并提供实际应用示例,确保开发者能够精准地控制产品展示。 在…

    2025年12月20日
    000
  • Shopify教程:高效筛选并展示特定Collection中的产品

    本文将围绕如何在Shopify Collection中筛选和展示特定产品展开。默认情况下,Shopify的Collection分页限制可能导致部分产品无法显示。本文将介绍如何使用Liquid模板语言的where过滤器,根据产品属性精确筛选产品,克服分页限制,确保所有符合条件的产品都能正确展示。 理解…

    2025年12月20日
    000
  • JavaScript的Object.freeze方法是什么?怎么用?

    object.freeze 是 javascript 中用于冻结对象的方法,它阻止添加、删除或修改对象的顶层属性,但对嵌套对象无效。1. 它接收一个对象并返回被冻结的对象;2. 冻结后,属性不可变,严格模式下修改会抛出错误;3. 实现的是浅冻结,嵌套对象仍可被修改。应用场景包括防止配置对象被篡改、提…

    2025年12月20日 好文分享
    000
  • BOM中如何操作浏览器的滚动条?

    控制浏览器滚动条的方法主要有:1.window.scrollto()设置绝对滚动位置;2.window.scrollby()进行相对滚动;3.element.scrollintoview()让元素滚动到可见区域;4.直接操作element.scrolltop和scrollleft属性。实现平滑滚动可…

    2025年12月20日 好文分享
    000
  • JavaScript的removeEventListener方法是什么?怎么用?

    removeeventlistener方法用于卸载之前通过addeventlistener绑定的事件监听器,避免内存泄漏和重复触发问题。使用时需注意三点:1.传入与添加时完全相同的事件类型、处理函数引用及第三个参数;2.避免使用匿名函数,否则无法移除;3.确保捕获/冒泡阶段参数一致。常见问题包括th…

    2025年12月20日 好文分享
    000
  • JavaScript的Date.prototype.getMinutes方法是什么?如何使用?

    getminutes() 方法返回本地时间的分钟数,用于提取 date 对象中的分钟信息以进行运算或展示。①调用方式是直接在 date 对象上调用,如 now.getminutes();②返回值为 0-59 的整数,可用于数学运算;③其返回本地时间而非 utc 时间,若需 utc 分钟应使用 get…

    2025年12月20日 好文分享
    000
  • Shopify教程:在集合列表中高效访问和筛选产品

    本文旨在解决Shopify Liquid模板中访问和筛选集合产品时遇到的问题,特别是当需要根据产品标题或其他属性进行特定产品展示时。我们将探讨如何克服默认分页限制,并使用where过滤器等技术更有效地筛选产品,从而确保在您的店铺中准确展示所需的产品。 在Shopify Liquid模板中,开发者经常…

    2025年12月20日
    000
  • Shopify教程:高效筛选和展示特定Collection中的商品

    本文档旨在解决Shopify Liquid模板中,从特定Collection中筛选并展示包含特定关键词的商品时遇到的问题。通过分析分页限制和where过滤器的使用,提供更精准的商品筛选方案,确保在店铺前端准确展示目标商品。 在使用Shopify Liquid模板开发店铺时,经常需要从Collecti…

    2025年12月20日
    000
  • async和await的基本用法解析

    async/await 的核心是简化异步操作写法,使代码更易读和维护。1. 它基于 promise,通过 async 声明函数,内部使用 await 暂停执行直到 promise resolve;2. 使用 try…catch 处理错误,提高可读性;3. 并发请求可通过 promise.…

    2025年12月20日 好文分享
    000
  • async函数在循环中的注意事项

    在循环中使用async函数需注意避免并发陷阱、控制执行顺序、处理数据竞争和错误。1. 并发执行可能导致结果不可预测,如数据竞争;2. 顺序执行可通过for…of或reduce实现,确保前一个任务完成后再执行下一个;3. 控制并发数量可使用并发池技术,限制同时运行的任务数;4. 错误处理应…

    2025年12月20日 好文分享
    000

发表回复

登录后才能评论
关注微信