ARTICLE AD BOX
Your useCallback implementation is certainly preferable to your useMemo version, but I think the memoization could be further improved.
One option could be to use an intermediate wrapping component that can instantiate the memoized action list internally and pass this down to the actual CDropdown component.
Example:
interface CDropdownWrapperProps { onDelete: /* whatever this type is */; value: DesignRecord; } const CDropdownOption = ({ onDelete, value }: CDropdownWrapperProps) => { const { t } = useTranslation(); const actionList = useMemo(() => [ { icon: PiPencilDuotone, label: t("edit"), path: `/planning/design/item/${value.id}`, }, { icon: PiTrashDuotone, label: t("_delete"), onClick: () => onDelete(value), } ], [onDelete, t, value]); return <CDropdown list={actionList} value={value} />; }; export function DesignList() { const [data] = useLoader(fetcher); ... return ( <table> {data.records.map((d) => ( <tr key={d.id}> <td> <CDropdownOption onDelete={handleDelete} value={d} /> </td> </tr> ))} </table> ); }Of course if you've full control over CDropdown you could just handle it directly inside it, passing the mapped record and the delete handler and handle creating the memoized action list.
Example:
interface CDropdownProps { onDelete: /* whatever this type is */; value: DesignRecord; } const CDropdown = ({ onDelete, value }: CDropdownProps) => { const { t } = useTranslation(); const actionList = useMemo(() => [ { icon: PiPencilDuotone, label: t("edit"), path: `/planning/design/item/${value.id}`, }, { icon: PiTrashDuotone, label: t("_delete"), onClick: () => onDelete(value), } ], [onDelete, t, value]); return /* dropdown JSX markup */; }; export function DesignList() { const [data] = useLoader(fetcher); ... return ( <table> {data.records.map((d) => ( <tr key={d.id}> <td> <CDropdown onDelete={handleDelete} value={d} /> </td> </tr> ))} </table> ); }Another alternative could be to keep the getActionList callback version but use the wrapper in the mapping callback to then instantiate and memoize the specific action list used in each row.
Example:
interface CDropdownWrapperProps { getActionList: (d: DesignRecord) => Option[]; value: DesignRecord; } const CDropdownOption = ({ getActionList, value }: CDropdownWrapperProps) => { const actionList = useMemo(() => getActionList(value), [getActionList, value]); return <CDropdown list={actionList} value={value} />; }; export function DesignList() { const [data] = useLoader(fetcher); const { t } = useTranslation(); const getActionList = useCallback((d: DesignRecord) => [ { icon: PiPencilDuotone, label: t("edit"), path: `/planning/design/item/${d.id}`, }, { icon: PiTrashDuotone, label: t("_delete"), onClick: () => handleDelete(d), } ], [handleDelete, t]); return ( <table> {data.records.map((d) => ( <tr key={d.id}> <td> <CDropdownOption getActionList={getActionList} value={d} /> </td> </tr> ))} </table> ); }The gist is to try pushing the computing of the memoized list of actions down the ReactTree closer to where they are used/needed and where you have access to all dependency values you want or need to memoize on.
