
React 的核心理念是声明式 UI。这意味着开发者通过描述 UI 的“状态”来构建界面,而不是直接操作 DOM 元素。React 会根据状态的变化自动、高效地更新 DOM。这种模式极大地简化了 UI 开发,并提高了应用的可维护性。
然而,当我们在 React 组件中直接使用诸如 document.querySelector、innerHTML 或 addEventListener 等原生 DOM API 时,就违背了 React 的声明式原则。这可能导致以下问题:
原始问题中的日历组件正是因为混合了命令式 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():
在 return 语句后调用 renderCalendar():
解决上述问题的核心在于拥抱 React 的状态驱动和声明式渲染范式。我们将通过 useState 和 useEffect 钩子来重构日历组件。
useState:管理组件状态
useEffect:处理副作用
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"
];
// ... (其他代码)
};将原 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 <= remainingSlots; i++) {
days.push({
date: i,
type: 'inactive',
key: `next-${i}`
});
}
// 更新状态
setDaysInMonth(days);
setCurrentDateText(`${months[currMonth]} ${currYear}`);
}, [currYear, currMonth, today]); // 依赖项:当年份或月份变化时重新计算
// 使用 useEffect 在组件挂载或 currYear/currMonth 变化时执行日历生成
useEffect(() => {
generateCalendarDays();
}, [generateCalendarDays]); // 依赖 generateCalendarDays 函数现在,我们可以利用 currentDateText 和 daysInMonth 这两个状态来渲染 UI。
return (
<div className="auth flexContainer">
<h1>Calendar</h1>
<div className="wrapper">
<header>
<p className="current-date">{currentDateText}</p> {/* 直接绑定状态 */}
<div className="icons">
<ArrowBackIosIcon id="prev" className="arrow" onClick={() => {
setCurrMonth(prevMonth => {
let newMonth = prevMonth - 1;
if (newMonth < 0) {
setCurrYear(prevYear => prevYear - 1);
return 11; // 11代表12月
}
return newMonth;
});
}}/>
<ArrowForwardIosIcon id="next" className="arrow" onClick={() => {
setCurrMonth(prevMonth => {
let newMonth = prevMonth + 1;
if (newMonth > 11) {
setCurrYear(prevYear => prevYear + 1);
return 0; // 0代表1月
}
return newMonth;
});
}}/>
</div>
</header>
<div className="calendar">
<ul className="weeks">
<li>Sunday</li>
<li>Monday</li>
<li>Tuesday</li>
<li>Wednesday</li>
<li>Thursday</li>
<li>Friday</li>
<li>Saturday</li>
</ul>
<ul className="days">
{daysInMonth.map(day => (
<li key={day.key} className={day.type}>{day.date}</li> // 映射数组生成列表
))}
</ul>
</div>
</div>
</div>
);
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 操作,拥抱状态驱动的详细内容,更多请关注php中文网其它相关文章!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号