React中工具函数用useContext及组件调用报错问题咨询
问题分析与解决方案
核心问题拆解
- 普通函数无法调用React钩子:你最初尝试在普通工具函数
calculatePercentage里直接使用useContext,违反了React钩子规则——钩子只能在组件或自定义钩子的顶层调用。 - 组件内钩子调用位置错误:修改为自定义钩子后,在
map循环里调用useCalculatePercentage,同样违反钩子规则,导致报错。
解决方案一:改造为合规的自定义钩子
将工具函数封装为返回计算逻辑的自定义钩子,在组件顶层调用钩子获取计算函数,再在循环中使用该函数。
1. 修复工具文件 calculatePercentage.js
import { addDays, differenceInCalendarDays } from "date-fns"; import { useContext } from "react"; import { TimelineSettingsContext } from "../TimelineSettingsProvider"; // 自定义钩子:返回一个可复用的计算函数 export function useCalculatePercentage() { // 在钩子顶层获取上下文,符合规则 const { timelineStart, timelineLength, timelineScale } = useContext(TimelineSettingsContext); // 返回纯计算逻辑的函数,供组件循环调用 return (startDate, endDate) => { // 修正startDate早于时间线起点的情况 const adjustedStartDate = differenceInCalendarDays(timelineStart, startDate) < 0 ? startDate : timelineStart; const startPosition = (differenceInCalendarDays(adjustedStartDate, timelineStart) / timelineLength) * timelineScale; const width = (differenceInCalendarDays(addDays(endDate, 1), adjustedStartDate) / timelineLength) * timelineScale; return { startPosition, width }; }; }
2. 修复Timeline组件调用逻辑
import { differenceInCalendarDays } from "date-fns"; import React, { useContext } from "react"; import BackgroundColumns from "./BackgroundColumns"; import BookingBar from "./BookingBar"; import { TimelineSettingsContext } from "../../TimelineSettingsProvider"; import { useCalculatePercentage } from "../../lib/calculatePercentage"; const Timeline = () => { const { groupedBookings, dates, setSelectedBooking } = useContext(TimelineSettingsContext); // 在组件顶层调用自定义钩子,获取计算函数(符合钩子规则) const calculatePercentage = useCalculatePercentage(); function handleBookingSelected(booking) { setSelectedBooking(booking.LfdNr); } return ( <div className="whitespace-nowrap relative"> <div className="absolute flex w-full h-full"> {dates?.map((date, index) => { // 调用顶层获取的计算函数,而非钩子 const { width } = calculatePercentage(new Date(), new Date()); return ( <BackgroundColumns key={index} date={date} width={width} ></BackgroundColumns> ); })} </div> <div className="relative flex flex-col z-20 pt-8"> {groupedBookings.map((category, index) => { return ( <React.Fragment key={index}> <div className="h-8" key={`${index}`} /> {category.bookings.map((booking, idx) => { // 同样调用计算函数 const { startPosition, width } = calculatePercentage( new Date(booking.Beginn_Datum), new Date(booking.Ende_Datum) ); const bookingLength = differenceInCalendarDays(booking.Ende_Datum, booking.Beginn_Datum) + 1; return ( <BookingBar callbackBarClicked={() => handleBookingSelected(booking)} key={booking.LfdNr} startPosition={startPosition} width={width} index={index} label={`${bookingLength} ${bookingLength > 1 ? "days" : "day"}`} /> ); })} </React.Fragment> ); })} </div> </div> ); }; export default Timeline;
解决方案二:纯函数传参(无钩子依赖)
如果希望计算逻辑脱离React上下文,可将上下文参数直接传入纯函数,更易测试和复用。
1. 工具文件改造
import { addDays, differenceInCalendarDays } from "date-fns"; // 纯函数:所有依赖通过参数传入,无React依赖 export function calculatePercentage(startDate, endDate, timelineStart, timelineLength, timelineScale) { const adjustedStartDate = differenceInCalendarDays(timelineStart, startDate) < 0 ? startDate : timelineStart; const startPosition = (differenceInCalendarDays(adjustedStartDate, timelineStart) / timelineLength) * timelineScale; const width = (differenceInCalendarDays(addDays(endDate, 1), adjustedStartDate) / timelineLength) * timelineScale; return { startPosition, width }; }
2. 组件调用
// 导入纯函数 import { calculatePercentage } from "../../lib/calculatePercentage"; const Timeline = () => { // 从上下文取出所需参数 const { groupedBookings, dates, setSelectedBooking, timelineStart, timelineLength, timelineScale } = useContext(TimelineSettingsContext); // ...其他代码 // 在循环中调用时传入参数 const { width } = calculatePercentage(new Date(), new Date(), timelineStart, timelineLength, timelineScale); }
方案选择建议
- 若计算逻辑仅在React组件中使用,且频繁依赖上下文,优先用自定义钩子方案。
- 若需要在非React环境复用计算逻辑,或追求更高的可测试性,选择纯函数传参方案。
内容的提问来源于stack exchange,提问作者beanCounter
相关产品推荐
相关产品推荐

