refactor: replace lodash imports with built-in or custom#8878
refactor: replace lodash imports with built-in or custom#8878
Conversation
Updated various components to use custom utility functions for string and array operations instead of lodash. This includes replacing `capitalize`, `startCase`, `sortBy`, `uniq`, and `range` with their respective implementations from local utility files. This change aims to reduce dependencies and improve code maintainability.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Refactors the frontend to reduce lodash-es usage by replacing common helpers (capitalize, startCase, sortBy, uniq, range, pick, isEqual, once, etc.) with locally defined utilities or smaller dedicated deps (dequal), aiming to shrink dependencies and centralize shared behavior.
Changes:
- Added/extended local utility helpers in
utils/strings.ts,utils/arrays.ts, andutils/objects.ts. - Replaced many
lodash-esimports across plugins, core state, and UI components with local utilities (anddequalfor deep equality). - Added unit tests for new array helpers.
Reviewed changes
Copilot reviewed 40 out of 40 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/utils/strings.ts | Adds capitalize and reimplements startCase, wiring Strings.startCase to the new implementation. |
| frontend/src/utils/objects.ts | Adds Objects.pick to replace lodash pick. |
| frontend/src/utils/arrays.ts | Adds range, uniq, sortBy, partition to replace lodash helpers. |
| frontend/src/utils/tests/arrays.test.ts | Adds unit tests for new array helpers. |
| frontend/src/plugins/impl/vega/params.ts | Switches uniq import to local utils/arrays. |
| frontend/src/plugins/impl/vega/loader.ts | Replaces lodash isNumber check with a native check in bigint parsing middleware. |
| frontend/src/plugins/impl/plotly/usePlotlyLayout.ts | Replaces lodash isEqual/pick usage with dequal + Objects.pick. |
| frontend/src/plugins/impl/plotly/PlotlyPlugin.tsx | Replaces lodash pick usage with Objects.pick for extracting Plotly point fields. |
| frontend/src/plugins/impl/data-frames/DataFramePlugin.tsx | Replaces lodash isEqual with dequal. |
| frontend/src/plugins/impl/data-explorer/functions/types.ts | Replaces lodash isString with typeof === "string". |
| frontend/src/plugins/impl/data-explorer/components/query-form.tsx | Replaces lodash startCase with Strings.startCase. |
| frontend/src/plugins/impl/data-editor/components.tsx | Replaces lodash capitalize with local capitalize. |
| frontend/src/plugins/impl/chat/chat-ui.tsx | Replaces lodash startCase with Strings.startCase. |
| frontend/src/plugins/impl/RangeSliderPlugin.tsx | Replaces lodash isEqual with dequal. |
| frontend/src/core/state/jotai.ts | Replaces lodash isEqual with dequal for deep-equal atoms. |
| frontend/src/core/saving/state.ts | Replaces lodash isEqual with dequal for save-dirty detection. |
| frontend/src/core/rtc/state.ts | Replaces lodash once with local utils/once. |
| frontend/src/core/network/requests-network.ts | Replaces lodash once with local utils/once. |
| frontend/src/core/codemirror/rtc/extension.ts | Replaces lodash isEqual with dequal and uses local once. |
| frontend/src/core/cells/effects.ts | Replaces lodash isEqual with dequal (keeps lodash debounce). |
| frontend/src/core/cells/cells.ts | Replaces lodash isEqual with dequal. |
| frontend/src/components/variables/variables-table.tsx | Replaces lodash sortBy with local sortBy. |
| frontend/src/components/editor/renderers/layout-select.tsx | Replaces lodash startCase with Strings.startCase. |
| frontend/src/components/editor/renderers/grid-layout/grid-layout.tsx | Replaces lodash startCase with Strings.startCase. |
| frontend/src/components/editor/connections/form-renderers.tsx | Replaces lodash partition with local partition. |
| frontend/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx | Replaces lodash startCase with Strings.startCase. |
| frontend/src/components/editor/actions/useNotebookActions.tsx | Replaces lodash startCase with Strings.startCase. |
| frontend/src/components/datasources/datasources.tsx | Replaces lodash sortBy with local sortBy. |
| frontend/src/components/data-table/pagination.tsx | Replaces lodash range with local range. |
| frontend/src/components/data-table/hooks/use-column-pinning.ts | Replaces lodash isEqual with dequal. |
| frontend/src/components/data-table/column-header.tsx | Replaces lodash capitalize with local capitalize. |
| frontend/src/components/data-table/charts/storage.ts | Replaces lodash capitalize with local capitalize. |
| frontend/src/components/data-table/charts/forms/common-chart.tsx | Replaces lodash capitalize with local capitalize. |
| frontend/src/components/data-table/charts/components/form-fields.tsx | Replaces lodash capitalize with local capitalize. |
| frontend/src/components/data-table/charts/components/chart-items.tsx | Replaces lodash capitalize with local capitalize. |
| frontend/src/components/chat/tool-call-accordion.tsx | Replaces lodash isEmpty with manual emptiness checks. |
| frontend/src/components/chat/acp/state.ts | Replaces lodash capitalize with local capitalize. |
| frontend/src/components/chat/acp/blocks.tsx | Updates imports to use local capitalize/Strings. |
| frontend/src/components/chat/acp/agent-panel.tsx | Replaces lodash capitalize with local capitalize and handles optional agent id. |
| frontend/src/components/ai/ai-model-dropdown.tsx | Replaces lodash capitalize with local capitalize. |
Comments suppressed due to low confidence (2)
frontend/src/components/chat/tool-call-accordion.tsx:131
ToolArgsRendererdetermines emptiness and object-ness viatypeof input === "object"+Object.keys(...). This misclassifies arrays (and other non-plain objects) and can render{}for inputs that aren’t actually empty objects. If you only want to special-case plain objects, consider usingArray.isArrayand a plain-object check; otherwise handle arrays separately (render[]when empty).
const isEmptyInput =
typeof input === "object" &&
Object.keys(input as Record<string, unknown>).length === 0;
const isObject =
typeof input === "object" &&
Object.keys(input as Record<string, unknown>).length > 0;
frontend/src/plugins/impl/vega/loader.ts:58
typeof parsedInt === "number"is always true here becauseNumber.parseIntalways returns anumber(includingNaN). This makes theelsebranch unreachable and suggests the check is not doing what it intends. If the goal is to avoidNaN, use!Number.isNaN(parsedInt)/Number.isFinite(parsedInt); otherwise the conditional can be removed for clarity.
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| pick<V extends Record<string, any>, K extends string>( | ||
| obj: V, | ||
| keys: readonly K[], | ||
| ): Pick<V, K & keyof V> { | ||
| const result = {} as Record<string, unknown>; | ||
| for (const key of keys) { | ||
| if (key in obj) { | ||
| result[key] = obj[key]; | ||
| } | ||
| } | ||
| return result as Pick<V, K & keyof V>; | ||
| }, |
There was a problem hiding this comment.
Objects.pick is a new utility but isn’t covered by the existing utils/__tests__/objects.test.ts suite. Adding a few unit tests (own vs inherited props, missing keys omitted, preserves undefined values for existing keys) would help ensure it behaves consistently with its call sites and the previous lodash behavior.
| {Object.entries(rest).map(([key, value]) => { | ||
| if (isEmpty(value)) { | ||
| if ( | ||
| value == null || | ||
| (typeof value === "object" && Object.keys(value).length === 0) | ||
| ) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The replacement for lodash isEmpty only checks value == null and Object.keys(value).length === 0 for objects. This will incorrectly treat non-plain objects as empty even when they contain data (e.g., new Map([['k','v']]) has Object.keys(...) === []), and it no longer treats empty strings as empty. Consider reintroducing an isEmpty helper that matches the needed semantics (handle strings/arrays/Map/Set distinctly, and treat other objects as empty only when they’re plain objects with no own keys).
| export function capitalize(str: string): string { | ||
| if (!str) { | ||
| return ""; | ||
| } | ||
| return str.charAt(0).toUpperCase() + str.slice(1); | ||
| } |
There was a problem hiding this comment.
capitalize is newly exported and used broadly (not just via Strings.startCase), but the utils/__tests__/strings.test.ts suite doesn’t include direct tests for it. Adding a small set of unit tests (empty string, already-capitalized, non-letter first char, mixed-case) would lock in the intended behavior and make future refactors safer.
| export function sortBy<T>( | ||
| arr: readonly T[], | ||
| key: (item: T) => string | number | undefined | null, | ||
| ): T[] { | ||
| return [...arr].sort((a, b) => { | ||
| const ka = key(a) ?? ""; | ||
| const kb = key(b) ?? ""; | ||
| if (ka < kb) { | ||
| return -1; | ||
| } | ||
| if (ka > kb) { | ||
| return 1; | ||
| } | ||
| return 0; | ||
| }); |
There was a problem hiding this comment.
sortBy coerces null/undefined keys to an empty string (key(a) ?? ""). This breaks numeric sort behavior when the key can be undefined (e.g., callers like cellIdToIndex.get(...)), because "" compares equal to 0 and will not reliably sort nullish values last (unlike lodash). Consider explicitly ordering null/undefined after non-nullish keys and comparing numbers vs strings in a type-safe way (e.g., handle nullish first, then number subtraction for numbers, else string localeCompare).
| export function sortBy<T>( | ||
| arr: readonly T[], | ||
| key: (item: T) => string | number | undefined | null, | ||
| ): T[] { | ||
| return [...arr].sort((a, b) => { | ||
| const ka = key(a) ?? ""; | ||
| const kb = key(b) ?? ""; | ||
| if (ka < kb) { | ||
| return -1; | ||
| } | ||
| if (ka > kb) { | ||
| return 1; | ||
| } | ||
| return 0; | ||
| }); | ||
| } |
There was a problem hiding this comment.
The new sortBy implementation doesn’t have a test case for null/undefined sort keys, which is a common usage pattern in this repo (e.g., sorting by Map.get(...)). Adding a unit test that asserts null/undefined keys sort to the end (and that numeric keys sort correctly) would prevent regressions here.
| const result = {} as Record<string, unknown>; | ||
| for (const key of keys) { | ||
| if (key in obj) { | ||
| result[key] = obj[key]; | ||
| } |
There was a problem hiding this comment.
Objects.pick uses key in obj, which will include properties found on the prototype chain. Lodash pick only selects an object’s own properties, so this can be a behavioral change and can accidentally include inherited keys. Prefer Object.prototype.hasOwnProperty.call(obj, key) (or Object.hasOwn(obj, key) if available) to restrict picks to own properties.
Bundle ReportChanges will decrease total bundle size by 810.58kB (-3.17%) ⬇️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: marimo-esmAssets Changed:
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
|
Return the original string value instead of empty string when Number.parseInt returns NaN, matching lodash's previous behavior. Also restructure as early return to satisfy linter.
Updated various components to use custom utility functions for string and array operations instead of lodash. This includes replacing
capitalize,startCase,sortBy,uniq, andrangewith their respective implementations from local utility files. This change aims to reduce dependencies and improve code maintainability.