Search code examples
next.jsreact-hooks

use a hook inside a function


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;


Solution

  • 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.