Skip to content

Memoized component with use-context-selector still rerenders #157

@piszczu4

Description

@piszczu4

I have the following component:

import { CalendarContext } from '@/app/(app)/account/lecture/calendar/calendar-provider';
import { SlotResizer } from '@/app/(app)/account/lecture/calendar/views/slots/slot-resizer';
import { useDragSlot } from '@/app/(app)/account/lecture/calendar/views/slots/use-drag-slot';
import { useResizeSlot } from '@/app/(app)/account/lecture/calendar/views/slots/use-resize-slot';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useTraceChange } from '@/hooks/use-trace-change';
import { formatDate } from '@/lib/utils';
import { LectureSlotRepeatMode, Slot } from '@/types';
import { Clock } from 'lucide-react';
import { memo } from 'react';
import { useContextSelector } from 'use-context-selector';

const Test = memo(({ slot }: { slot: Slot }) => {
  console.log('Rerender');

  const color = CALENDAR_SLOT_ITEMS.find(
    (el) => el.type === slot.repeatMode
  )!.color;

  const mode = useContextSelector(CalendarContext, (ctx) => ctx.mode);

  return <div>DIV</div>;
});

const CALENDAR_SLOT_ITEMS = [
  {
    type: LectureSlotRepeatMode.DAY,
    color: 'green',
  },
  {
    type: LectureSlotRepeatMode.WEEK,
    color: 'blue',
  },
  {
    type: LectureSlotRepeatMode.NO_REPEAT,
    color: 'violet',
  },
];

Test.displayName = 'Test';

export const CalendarSlotEventInner = memo(({ slot }: { slot: Slot }) => {
  const color = CALENDAR_SLOT_ITEMS.find(
    (el) => el.type === slot.repeatMode
  )!.color;

  const getEventStyles = useContextSelector(
    CalendarContext,
    (ctx) => ctx.getEventStyles
  );
  const mode = useContextSelector(CalendarContext, (ctx) => ctx.mode);
  const { height } = getEventStyles(slot);

  const {
    isResizingBottom,
    isResizingTop,
    topResizerRef,
    bottomResizerRef,
    onMouseDownResizer,
  } = useResizeSlot(slot);

  const { dragging, slotRef, onMouseDown, newTop, translateX } =
    useDragSlot(slot);

  return (
    <div
      ref={slotRef}
      className='absolute z-[4] flex w-full flex-col gap-1 overflow-hidden border-x-[3px]'
      style={{
        top: newTop,
        height,
        borderColor: `var(--${color}8)`,
        backgroundColor: `var(--${color}1)`,
        transform: translateX ? `translateX(${translateX}px)` : undefined,
      }}
    >
      {mode === 'slots' && (
        <SlotResizer
          onMouseDown={(e) => onMouseDownResizer(e, true)}
          ref={topResizerRef}
          dir='up'
          slot={slot}
        />
      )}
      <ScrollArea
        className='flex-1'
        onMouseDown={onMouseDown}
        style={{
          cursor:
            isResizingTop || isResizingBottom
              ? 'ns-resize'
              : dragging
                ? 'grabbing'
                : mode === 'slots'
                  ? 'grab'
                  : 'default',
        }}
      >
        <div className='p-2'>
          <span className='flex items-center gap-1.5 text-xs'>
            <Clock className='h-4 w-4 shrink-0' />
            <span className='font-bold'>
              {formatDate(slot.fromDate, 'HH:mm')} -{' '}
              {formatDate(slot.toDate, 'HH:mm')}
            </span>
          </span>
          <span className='flex items-center gap-1.5'>
            {/* <Repeat className='h-4 w-4 shrink-0' /> */}
            {/* {slot.repeatMode === LectureSlotRepeatMode.NO_REPEAT
                  ? 'Brak'
                  : slot.repeatMode === LectureSlotRepeatMode.WEEK
                    ? '1 W'
                    : '1 BD'} */}
            {/* <span>
                  {
                    getSelectableRepeatModes().find(
                      (el) => el.value === slot.repeatMode
                    )?.label
                  }
                </span> */}
          </span>
        </div>
      </ScrollArea>
      {mode === 'slots' && (
        <SlotResizer
          ref={bottomResizerRef}
          slot={slot}
          dir='down'
          onMouseDown={(e) => onMouseDownResizer(e, false)}
        />
      )}
    </div>
  );
});

CalendarSlotEventInner.displayName = 'CalendarSlotEventInner';

export const CalendarSlotEvent = memo(({ slot }: { slot: Slot }) => {
  if (slot.id !== '69fc4516-970c-4a00-92f1-3fa073cbe4d8')
    return <Test slot={slot} />;

  return <CalendarSlotEventInner slot={slot} />;
  // return (
  //   <Popover>
  //     <PopoverTrigger asChild>
  //     </PopoverTrigger>
  //     <PopoverContent
  //       className='p-0'
  //       onOpenAutoFocus={(e) => e.preventDefault()}
  //     >
  //       <SlotCard slot={slot} editable={mode === 'slots'} />
  //     </PopoverContent>
  //   </Popover>
  // );
});

What is happening here is the following:

  • CalendarSlotEvent is my main component and I render it for every event in my calendar
  • I want to render CalendarSlotEventInner for every slot but there are renderes that I dont understand so I render CalendarSlotEventInner only for single event (to be able to resize it) and for others I render Test component.

When I resize any event, I dont want to rerender other event. For that Im using use-context-selector to prevent rerenders when value from context does not change, but Test still rerenders... When I remove

  const mode = useContextSelector(CalendarContext, (ctx) => ctx.mode);

then Test is correctly not rerendered. In my contxt I have:

  const getEventStyles = useCallback(
    (event: { fromDate: Date; toDate: Date }) => {
      const fromDate = event.fromDate;
      const fromMinutes =
        fromDate.getHours() * 60 + fromDate.getMinutes() - hourRange[0] * 60;
      const top = fromMinutes * minuteHeight;
      const height =
        differenceInMinutes(event.toDate, event.fromDate) * minuteHeight;

      return { top, height };
    },
    [hourRange, minuteHeight] // zależności, jeśli się zmieniają, to funkcja się zaktualizuje
  );

where

  const [mode, setMode] = useState<CalendarMode>(
    user.role === Role.TEACHER ? 'both' : 'bookings'
  );

and my context returns

 <DndProvider backend={HTML5Backend}>
      <CalendarContext.Provider
        value={{
          bookings: bookings
            ? bookings.results.filter(
                (el) => el.status !== BookingStatus.CANCELLED
              )
            : [],
          slots,
          setSlots,
          date,
          setDate,
          days,
          view,
          setView,
          dateRange,
          setDateRange,
          onDateChange,
          isFullscreenMode,
          setIsFullscreenMode,
          hourHeight,
          setHourHeight,
          hourRange,
          setHourRange,
          hourScale,
          showSlots,
          setShowSlots,
          showBookings,
          setShowBookings,
          getEventStyles,
          minuteHeight,
          mode,
          onModeChange,
          stepLength,
          setStepLength,
          showWeekend,
          setShowWeekend,
        }}
      >
        {children}
      </CalendarContext.Provider>
    </DndProvider>

Any suggestions why the Test component rerenders? As you can see my context is huge and chatGPT suggested that the reference to mode is changing each time (during resize, slots state i updated) and this is beacuse use-context-selector does not work properly. But how then fix that? I dont want to have 10 smaller context for every state :(

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions