如何在React.js项目中基于JavaScript实现自定义日历?
React 自定义日历实现方案

我在React项目里做过好几款和图示效果差不多的自定义日历,整体没有特别复杂的技术点,给你梳理可直接落地的实现思路:
基础选型
- 日期计算逻辑直接用
date-fns库就行,比moment轻量很多,计算当月起止日期、周偏移、相邻月日期、日期对比这些场景都有现成API,不用自己处理闰年、大小月天数的边界问题,能避开90%的日历bug。 - 如果要做范围选择、多日期选中、日期禁用这类复杂交互,可以用
react-day-picker做无样式的底层逻辑层,样式完全自己控制,比硬改antd、MUI这类组件库自带日历的默认样式省事很多;如果需求不复杂完全自己写状态也没多少代码。
组件拆分逻辑
整个日历拆成3个核心模块就行,状态统一存在最外层的日历父组件:
- 头部导航区:放上下月/上下年切换按钮、当前展示年月的标题,维护一个
currentMonth状态存当前视图对应月份的第一天Date对象,点切换按钮时更新这个状态即可。 - 星期表头区:固定循环渲染周几的文案就行,不需要动态逻辑,根据产品需求决定周首是周日还是周一。
- 日期网格区:这部分是核心,固定渲染6行*7列共42个日期格子,保证切换月份的时候日历高度固定不会抖动:
- 基于
currentMonth算出当月第一天是周几,先在网格最前面补全对应数量的上月末尾日期 - 依次填入当月1号到当月最后一天的日期
- 剩下的空位补全下月开头的日期,凑够42个格子
- 每个格子单独判断状态:是否为选中日期/选中范围的起止/选中范围内的日期、是否为当天、是否为相邻月的灰显日期、是否为禁用日期,给不同状态的格子挂对应的类名即可。如果要做图示里的日期下的标记、日程提示,给格子留自定义渲染的插槽,传入以日期为key的配置对象做匹配渲染就行。
- 基于
交互与样式注意点
- 布局直接用CSS Grid实现日期网格,设置
grid-template-columns: repeat(7, 1fr)就能自动均分7列宽度,比flex调间距方便很多。 - 日期选择如果是单选就维护一个
selectedDate状态存选中的Date对象;如果是范围选就维护rangeStart、rangeEnd两个状态,第一次点击赋值给start,第二次点击时如果日期晚于start就赋值给end,早于start就重置start为当前点击的日期即可。 - 切月的时候可以给网格加个简单的淡入淡出或者位移动画,体验会好很多。
最小实现核心代码参考
import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, addDays, format, isSameMonth, isSameDay } from 'date-fns' import { useState } from 'react' const CustomCalendar = () => { // 当前视图展示的月份 const [currentMonth, setCurrentMonth] = useState(new Date()) // 当前选中的日期 const [selectedDate, setSelectedDate] = useState(new Date()) // 生成日历网格需要的42个日期 const renderCalendarCells = () => { const monthStart = startOfMonth(currentMonth) const calendarStart = startOfWeek(monthStart) const calendarEnd = endOfWeek(endOfMonth(monthStart)) const cellList = [] let currentDay = calendarStart while (currentDay <= calendarEnd) { cellList.push(currentDay) currentDay = addDays(currentDay, 1) } return cellList } return ( <div className="custom-calendar-wrapper"> {/* 头部导航 */} <div className="calendar-header"> <button onClick={() => setCurrentMonth(addDays(currentMonth, -30))}> 上月 </button> <span className="current-month-text"> {format(currentMonth, 'yyyy年MM月')} </span> <button onClick={() => setCurrentMonth(addDays(currentMonth, 30))}> 下月 </button> </div> {/* 星期表头 */} <div className="week-header-row"> {['日','一','二','三','四','五','六'].map(weekText => ( <div className="week-item" key={weekText}>{weekText}</div> ))} </div> {/* 日期网格 */} <div className="days-grid"> {renderCalendarCells().map((dayItem, index) => ( <div key={index} className={` day-cell ${!isSameMonth(dayItem, currentMonth) ? 'gray-other-month' : ''} ${isSameDay(dayItem, selectedDate) ? 'selected-style' : ''} ${isSameDay(dayItem, new Date()) ? 'is-today' : ''} `} onClick={() => setSelectedDate(dayItem)} > <span className="day-number">{format(dayItem, 'd')}</span> {/* 这里可以加自定义标记、日程提示的渲染逻辑 */} </div> ))} </div> </div> ) } export default CustomCalendar
剩下的样式部分你对着参考图调整颜色、圆角、间距就行,没有特殊的实现难点。
内容的提问来源于stack exchange,提问作者Abdulaziz
相关产品推荐
相关产品推荐

