I'm a beginner in react and JS. I want to refactor and clean up my endless files a little and seperating in files for different tasks. So I created a lib folder and created a file called calculatePercentage.js and I want to use a useContext hook there. How is this possible to achieve? Do I need to create a component?
this is not possible:
import { addDays, differenceInCalendarDays } from "date-fns";
import { useContext } from "react";
import { TimelineSettingsContext } from "../TimelineSettingsProvider";
// Function to calculate the percentage difference between two dates
export function calculatePercentage(startDate, endDate) {
const { timelineStart, timelineLength, timelineScale } = useContext(
TimelineSettingsContext
);
//wenn startDate vor dem TimelineStart liegt, dass muss der Balken verkürzt werden und startet bei timelineStart
startDate =
differenceInCalendarDays(timelineStart, startDate) < 0
? startDate
: timelineStart;
const startPosition =
(differenceInCalendarDays(startDate, timelineStart) / timelineLength) *
timelineScale;
const width =
(differenceInCalendarDays(addDays(endDate, 1), startDate) /
timelineLength) *
timelineScale;
return { startPosition, width };
}
Now after solving that error with comment below. Getting another error from the calling component:
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
);
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 } = useCalculatePercentage(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}`} // Verwenden Sie eine eindeutige Kombination aus index und idx für den key
/>
{category.bookings.map((booking, idx) => {
const { startPosition, width } = useCalculatePercentage(
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} // Verwenden Sie eine eindeutige Kombination aus index und idx für den key
startPosition={startPosition}
width={width}
index={index}
label={`${bookingLength} ${
bookingLength > 1 ? "days" : "day"
}`}
/>
);
})}
</React.Fragment>
);
})}
</div>
</div>
);
};
export default Timeline;
Yeah, as you noticed, you can't use hooks in functions. React hooks can only be called inside a function component or from another custom hook. So when dealing with non JSX returns you would make another hook from within which you can call the hooks you need to use. So in this case you would likely write something like
export function useCalculatePercentage(startDate, endDate) {
const { timelineStart, timelineLength, timelineScale } = useContext(
TimelineSettingsContext,
);
// ...
return { startPosition, width };
}
And then you have a new hook that you can use to get the percentage value as you need. And that again needs to be called at the top level in a function component or another hook.
In your case, where you use useCalculatePercentage
it will not work as intended or it will give warnings. You should call the hook in the top level of the function, so no loops, no callbacks, just somewhere here
const Timeline = () => {
const { groupedBookings, dates, setSelectedBooking } = useContext(
TimelineSettingsContext
);
//<--- call hook
function handleBookingSelected(booking) {
setSelectedBooking(booking.LfdNr);
}
//<--- or here
I think its something to do with how react keeps track of the hooks, so essentially one of the main things is that the number of calls to the hook must be consistent across renderers, and even sometimes calling the hooks in maps etc like in your example can yield a consistent number of hook calls over the time period it is required but it's no guarantee so react warns against it. So its kind of going against the design of react so not worth the headache.
In your situation, for the first call, it's easy to move the call to the hook outside of any callbacks since the arguments are just current date so it's the same for all elements that would be iterated over if in the map function. But the second callback is more complicated. But if you want to use a hook here, you could return a function from the hook. So basically the hook itself can get all the variables required from react, so other hooks, including context values like in your case and return a function that takes regular variables and then returns the value. So could look something like
export function useCalculatePercentageWithContext() {
const { timelineStart, timelineLength, timelineScale } = useContext(
TimelineSettingsContext,
);
const calculatePercentage = (start, end) => {
//...
return { startPosition, width };
};
return calculatePercentage;
}
and since calculate percentage is just a normal function you can call that wherever you want, so would use like
const Timeline = () => {
const { groupedBookings, dates, setSelectedBooking } = useContext(
TimelineSettingsContext
);
const calculatePercentage = useCalculatePercentageWithContext();
//....
{category.bookings.map((booking, idx) => {
const { startPosition, width } = calculatePercentage(
new Date(booking.Beginn_Datum),new Date(booking.Ende_Datum))...}
And this will work fine, but in my opinion it can quickly becomes annoying to have all these hooks, and every time you want to add some functionality you have to make another hook that uses some hooks etc..
If I were refactoring this I would call the context hook in the component and have a normal function somewhere and simply pass it the arguments, including those from context. Something like
export function calculatePercentage({
timelineStart,
timelineLength,
timelineScale,
startDate,
endDate,
}) {
///... logic
return { startPosition, width };
}
then in timeline
const { timelineStart, timelineLength, timelineScale } = useContext(
TimelineSettingsContext,
);
and
{category.bookings.map((booking, idx) => {
const { startPosition, width } = calculatePercentage({
start:new Date(booking.Beginn_Datum),
end:new Date(booking.Ende_Datum),
timelineStart,
timelineLength,
timelineScale,
});
and I think will be a lot simpler like this.