Skip to content

react-through is deprecated and not supported in React 19 anymore #76

@Jef47

Description

@Jef47

Since ThroughProvider isn't supported in React 19 anymore, we made a more modern implementation of de breadcrumbs, using createContext. You can then simply replace ThroughProvider by BreadcrumbsProvider .

import React, { createContext, useContext, useState, ReactNode, forwardRef } from 'react';

interface BaseBreadcrumbItem {
  children?: ReactNode;
  [key: string]: any;
}

interface BreadcrumbItem extends BaseBreadcrumbItem {
  to: string;
}

interface BreadcrumbRecord {
  key: string;
  item: BreadcrumbItem;
}

type BreadcrumbsCollection = Record<string, BreadcrumbItem>;

interface BreadcrumbsContextType {
  items: BreadcrumbsCollection;
  addItem: (record: BreadcrumbRecord) => void;
  removeItem: (key: string) => void;
}

const BreadcrumbsContext = createContext<BreadcrumbsContextType | undefined>(undefined);

interface BreadcrumbsProviderProps {
  children: ReactNode;
}

export function BreadcrumbsProvider({ children }: BreadcrumbsProviderProps) {
  const [items, setItems] = useState<BreadcrumbsCollection>({});
  const itemsRef = React.useRef(items);

  React.useEffect(() => {
    itemsRef.current = items;
  }, [items]);

  const addItem = React.useCallback((record: BreadcrumbRecord) => {
    setItems((prev) => {
      if (prev[record.key] && prev[record.key].to === record.item.to && prev[record.key].children === record.item.children) {
        return prev;
      }
      return { ...prev, [record.key]: record.item };
    });
  }, []);

  const removeItem = React.useCallback((key: string) => {
    setItems((prev) => {
      if (!prev[key]) return prev;
      const newItems = { ...prev };
      delete newItems[key];
      return newItems;
    });
  }, []);

  return <BreadcrumbsContext.Provider value={{ items, addItem, removeItem }}>{children}</BreadcrumbsContext.Provider>;
}

export function useBreadcrumbs() {
  const context = useContext(BreadcrumbsContext);
  if (context === undefined) {
    throw new Error('useBreadcrumbs must be used within a BreadcrumbsProvider');
  }
  return context;
}

interface BreadcrumbsItemProps extends BreadcrumbItem {
  to: string;
  children?: ReactNode;
}

export function BreadcrumbsItem({ children, ...props }: BreadcrumbsItemProps) {
  const { addItem, removeItem } = useBreadcrumbs();
  const itemKey = props.to;

  const mountedRef = React.useRef(false);

  React.useEffect(() => {
    if (!mountedRef.current) {
      mountedRef.current = true;
    }

    addItem({ key: itemKey, item: { ...props, children } });

    return () => {
      if (mountedRef.current) {
        removeItem(itemKey);
      }
    };
  }, [itemKey, children]);

  return null;
}

function prepareProps(props: Record<string, any>, rename: Record<string, string> = {}, duplicate: Record<string, string> = {}, remove: Record<string, boolean> = {}) {
  const p = { ...props };

  Object.entries(duplicate).forEach(([k, v]) => {
    p[v] = p[k];
  });

  Object.entries(rename).forEach(([k, v]) => {
    p[v] = p[k];
    delete p[k];
  });

  Object.keys(remove).forEach((k) => {
    delete p[k];
  });

  return p;
}

const DefaultLink = forwardRef<HTMLAnchorElement, any>((props, ref) => <a ref={ref} {...props} />);

const DefaultContainer = forwardRef<HTMLSpanElement, any>((props, ref) => <span ref={ref} {...props} />);

interface BreadcrumbsProps {
  container?: React.ElementType;
  containerProps?: Record<string, any>;
  hideIfEmpty?: boolean;
  item?: React.ElementType;
  finalItem?: React.ElementType;
  finalProps?: Record<string, any>;
  separator?: ReactNode;
  duplicateProps?: Record<string, string>;
  removeProps?: Record<string, boolean>;
  renameProps?: Record<string, string>;
  compare?: (a: BreadcrumbItem, b: BreadcrumbItem) => number;
}

export const Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(
  (
    {
      container: Container = DefaultContainer,
      containerProps = {},
      hideIfEmpty = false,
      item: Item = DefaultLink,
      finalItem: FinalItem = Item,
      finalProps = {},
      separator,
      duplicateProps = {},
      removeProps = {},
      renameProps = Item === DefaultLink ? { to: 'href' } : {},
      compare,
    },
    ref,
  ) => {
    const { items } = useBreadcrumbs();

    const defaultCompare = (a: BreadcrumbItem, b: BreadcrumbItem) => a.to.length - b.to.length;

    const itemsValue = Object.values(items).sort(compare || defaultCompare);
    const count = itemsValue.length;

    if (hideIfEmpty && count === 0) {
      return null;
    }

    return (
      <Container ref={ref} {...containerProps}>
        {itemsValue.map((itemValue, i) => {
          const isFinal = i + 1 === count;
          const ItemComponent = isFinal ? FinalItem : Item;
          const { children, ...restItemValue } = itemValue;
          const itemProps = prepareProps(restItemValue, renameProps, duplicateProps, removeProps);
          const uniqueKey = `${itemValue.to}-${i}`;

          return separator && !isFinal ? (
            <span key={uniqueKey}>
              <ItemComponent {...itemProps}>{children}</ItemComponent>
              {separator}
            </span>
          ) : (
            <ItemComponent key={uniqueKey} {...itemProps} {...(isFinal ? finalProps : {})}>
              {children}
            </ItemComponent>
          );
        })}
      </Container>
    );
  },
);

Breadcrumbs.displayName = 'Breadcrumbs';

export type { BreadcrumbItem, BreadcrumbsContextType, BreadcrumbsProps, BreadcrumbsCollection, BreadcrumbRecord };

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