-
Notifications
You must be signed in to change notification settings - Fork 30
Open
Description
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 };
duponter
Metadata
Metadata
Assignees
Labels
No labels