From e0fadfee7c5acac54b2c7da6ce5a880605b93202 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Mon, 22 Jun 2026 13:33:32 +0200
Subject: [PATCH] Remove jotai from twenty-ui (#21937)
twenty-ui no longer depends on jotai, so its components work without a
consumer-provided jotai store (better practice for a shared UI library).
twenty-front keeps jotai; this is scoped to the library.
- **Avatar**: tracks image-load failure in local `useState` instead of a
global atom.
- **Icons**: the icon registry moved from a jotai atom to a React
Context. `IconsProvider` and `useIcons` keep identical signatures; the
context itself stays internal.
- Removed the unused `createState` helper, the `invalidAvatarUrlsAtomV2`
/ `iconsState` atoms, and `JotaiRootDecorator`; regenerated barrels and
dropped the `jotai` dependency.
No other package needs changes: nothing imports the removed symbols, and
`twenty-sdk` (which re-exports twenty-ui via `export *`) simply stops
surfacing the two leaked atoms on its next publish.
---
packages/twenty-ui/package.json | 1 -
.../twenty-ui/src/data-display/Avatar/Avatar.tsx | 14 ++++++--------
.../Avatar/__stories__/Avatar.stories.tsx | 8 ++------
.../Avatar/states/invalidAvatarUrlsAtomV2.ts | 4 ----
.../__stories__/AvatarGroup.stories.tsx | 5 ++---
.../__stories__/AvatarOrIcon.stories.tsx | 8 ++------
packages/twenty-ui/src/data-display/index.ts | 1 -
.../icon/components/__stories__/Icon.stories.tsx | 2 --
.../{useIcons.test.ts => useIcons.test.tsx} | 14 ++++++++------
packages/twenty-ui/src/icon/hooks/useIcons.ts | 6 +++---
packages/twenty-ui/src/icon/index.ts | 1 -
.../twenty-ui/src/icon/internal/IconsContext.ts | 5 +++++
.../twenty-ui/src/icon/providers/IconsProvider.tsx | 14 ++++++++------
packages/twenty-ui/src/icon/states/iconsState.ts | 6 ------
.../TabButton/__stories__/TabButton.stories.tsx | 3 +--
.../MenuItemMultiSelectAvatar.stories.tsx | 5 ++---
.../__stories__/MenuItemSelectAvatar.stories.tsx | 3 ---
.../src/testing/decorators/JotaiRootDecorator.tsx | 9 ---------
packages/twenty-ui/src/testing/index.ts | 1 -
packages/twenty-ui/src/utilities/index.ts | 1 -
.../src/utilities/state/utils/createState.ts | 13 -------------
yarn.lock | 1 -
22 files changed, 39 insertions(+), 86 deletions(-)
delete mode 100644 packages/twenty-ui/src/data-display/Avatar/states/invalidAvatarUrlsAtomV2.ts
rename packages/twenty-ui/src/icon/hooks/__tests__/{useIcons.test.ts => useIcons.test.tsx} (80%)
create mode 100644 packages/twenty-ui/src/icon/internal/IconsContext.ts
delete mode 100644 packages/twenty-ui/src/icon/states/iconsState.ts
delete mode 100644 packages/twenty-ui/src/testing/decorators/JotaiRootDecorator.tsx
delete mode 100644 packages/twenty-ui/src/utilities/state/utils/createState.ts
diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json
index 175007843c..c0fbc76005 100644
--- a/packages/twenty-ui/package.json
+++ b/packages/twenty-ui/package.json
@@ -76,7 +76,6 @@
"date-fns": "^4.4.0",
"framer-motion": "^11.18.0",
"glob": "^11.1.0",
- "jotai": "^2.17.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-responsive": "^9.0.2",
diff --git a/packages/twenty-ui/src/data-display/Avatar/Avatar.tsx b/packages/twenty-ui/src/data-display/Avatar/Avatar.tsx
index 944e03beb7..87423aa3c3 100644
--- a/packages/twenty-ui/src/data-display/Avatar/Avatar.tsx
+++ b/packages/twenty-ui/src/data-display/Avatar/Avatar.tsx
@@ -1,10 +1,8 @@
import { isNonEmptyString, isNull, isUndefined } from '@sniptt/guards';
import { clsx } from 'clsx';
-import { useAtom } from 'jotai';
-import { useContext } from 'react';
+import { useContext, useState } from 'react';
import { handleClickableElementKeyDown } from '@ui/accessibility/utils/handleClickableElementKeyDown';
-import { invalidAvatarUrlsAtomV2 } from '@ui/data-display/Avatar/states/invalidAvatarUrlsAtomV2';
import { type AvatarSize } from '@ui/data-display/Avatar/types/AvatarSize';
import { type AvatarType } from '@ui/data-display/Avatar/types/AvatarType';
import { type IconComponent } from '@ui/icon/types/IconComponent';
@@ -47,9 +45,9 @@ export const Avatar = ({
}: AvatarProps) => {
const { theme } = useContext(ThemeContext);
- const [invalidAvatarUrls, setInvalidAvatarUrls] = useAtom(
- invalidAvatarUrlsAtomV2,
- );
+ const [erroredAvatarImageURI, setErroredAvatarImageURI] = useState<
+ string | null
+ >(null);
const avatarImageURI = isNonEmptyString(avatarUrl)
? getImageAbsoluteURI({
@@ -64,11 +62,11 @@ export const Avatar = ({
const placeholderChar = placeholderFirstChar?.toUpperCase() || '-';
const showPlaceholder =
- isNull(avatarImageURI) || invalidAvatarUrls.includes(avatarImageURI);
+ isNull(avatarImageURI) || erroredAvatarImageURI === avatarImageURI;
const handleImageError = () => {
if (isNonEmptyString(avatarImageURI)) {
- setInvalidAvatarUrls((prev) => [...prev, avatarImageURI]);
+ setErroredAvatarImageURI(avatarImageURI);
}
};
diff --git a/packages/twenty-ui/src/data-display/Avatar/__stories__/Avatar.stories.tsx b/packages/twenty-ui/src/data-display/Avatar/__stories__/Avatar.stories.tsx
index 38fb5c2c87..c7117e093a 100644
--- a/packages/twenty-ui/src/data-display/Avatar/__stories__/Avatar.stories.tsx
+++ b/packages/twenty-ui/src/data-display/Avatar/__stories__/Avatar.stories.tsx
@@ -1,17 +1,13 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
-import {
- AVATAR_URL_MOCK,
- ComponentDecorator,
- JotaiRootDecorator,
-} from '@ui/testing';
+import { AVATAR_URL_MOCK, ComponentDecorator } from '@ui/testing';
import { Avatar } from '@ui/data-display/Avatar/Avatar';
const meta: Meta = {
title: 'UI/Data Display/Avatar',
component: Avatar,
- decorators: [ComponentDecorator, JotaiRootDecorator],
+ decorators: [ComponentDecorator],
args: {
avatarUrl: AVATAR_URL_MOCK,
size: 'md',
diff --git a/packages/twenty-ui/src/data-display/Avatar/states/invalidAvatarUrlsAtomV2.ts b/packages/twenty-ui/src/data-display/Avatar/states/invalidAvatarUrlsAtomV2.ts
deleted file mode 100644
index 94ae8d03b3..0000000000
--- a/packages/twenty-ui/src/data-display/Avatar/states/invalidAvatarUrlsAtomV2.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { atom } from 'jotai';
-
-export const invalidAvatarUrlsAtomV2 = atom([]);
-invalidAvatarUrlsAtomV2.debugLabel = 'invalidAvatarUrlsAtomV2';
diff --git a/packages/twenty-ui/src/data-display/AvatarGroup/__stories__/AvatarGroup.stories.tsx b/packages/twenty-ui/src/data-display/AvatarGroup/__stories__/AvatarGroup.stories.tsx
index f65790115f..e18b57d0f3 100644
--- a/packages/twenty-ui/src/data-display/AvatarGroup/__stories__/AvatarGroup.stories.tsx
+++ b/packages/twenty-ui/src/data-display/AvatarGroup/__stories__/AvatarGroup.stories.tsx
@@ -8,7 +8,6 @@ import {
AVATAR_URL_MOCK,
CatalogDecorator,
ComponentDecorator,
- JotaiRootDecorator,
} from '@ui/testing';
import {
@@ -43,7 +42,7 @@ export default meta;
type Story = StoryObj;
export const Default: Story = {
- decorators: [ComponentDecorator, JotaiRootDecorator],
+ decorators: [ComponentDecorator],
};
export const Catalog: Story = {
@@ -69,5 +68,5 @@ export const Catalog: Story = {
],
},
},
- decorators: [CatalogDecorator, JotaiRootDecorator],
+ decorators: [CatalogDecorator],
};
diff --git a/packages/twenty-ui/src/data-display/AvatarOrIcon/__stories__/AvatarOrIcon.stories.tsx b/packages/twenty-ui/src/data-display/AvatarOrIcon/__stories__/AvatarOrIcon.stories.tsx
index cc98012282..9a231b26c9 100644
--- a/packages/twenty-ui/src/data-display/AvatarOrIcon/__stories__/AvatarOrIcon.stories.tsx
+++ b/packages/twenty-ui/src/data-display/AvatarOrIcon/__stories__/AvatarOrIcon.stories.tsx
@@ -1,16 +1,12 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconBuildingSkyscraper, IconUser } from '@ui/icon';
-import {
- AVATAR_URL_MOCK,
- ComponentDecorator,
- JotaiRootDecorator,
-} from '@ui/testing';
+import { AVATAR_URL_MOCK, ComponentDecorator } from '@ui/testing';
import { AvatarOrIcon } from '@ui/data-display/AvatarOrIcon/AvatarOrIcon';
const meta: Meta = {
title: 'UI/Data Display/AvatarOrIcon',
component: AvatarOrIcon,
- decorators: [ComponentDecorator, JotaiRootDecorator],
+ decorators: [ComponentDecorator],
};
export default meta;
diff --git a/packages/twenty-ui/src/data-display/index.ts b/packages/twenty-ui/src/data-display/index.ts
index 0f217f1482..66b0ea7211 100644
--- a/packages/twenty-ui/src/data-display/index.ts
+++ b/packages/twenty-ui/src/data-display/index.ts
@@ -12,7 +12,6 @@ export { AnimatedCheckmark } from './AnimatedCheckmark/AnimatedCheckmark';
export type { AvatarProps } from './Avatar/Avatar';
export { Avatar } from './Avatar/Avatar';
export { AVATAR_PROPERTIES_BY_SIZE } from './Avatar/constants/AvatarPropertiesBySize';
-export { invalidAvatarUrlsAtomV2 } from './Avatar/states/invalidAvatarUrlsAtomV2';
export type { AvatarSize } from './Avatar/types/AvatarSize';
export type { AvatarType } from './Avatar/types/AvatarType';
export type { AvatarGroupProps } from './AvatarGroup/AvatarGroup';
diff --git a/packages/twenty-ui/src/icon/components/__stories__/Icon.stories.tsx b/packages/twenty-ui/src/icon/components/__stories__/Icon.stories.tsx
index 03ad27c0ce..8b540ef590 100644
--- a/packages/twenty-ui/src/icon/components/__stories__/Icon.stories.tsx
+++ b/packages/twenty-ui/src/icon/components/__stories__/Icon.stories.tsx
@@ -2,7 +2,6 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconsProvider } from '@ui/icon/providers/IconsProvider';
import { ComponentDecorator } from '@ui/testing/decorators/ComponentDecorator';
-import { JotaiRootDecorator } from '@ui/testing/decorators/JotaiRootDecorator';
import { Icon } from '../Icon';
@@ -16,7 +15,6 @@ const meta: Meta = {
),
- JotaiRootDecorator,
],
};
diff --git a/packages/twenty-ui/src/icon/hooks/__tests__/useIcons.test.ts b/packages/twenty-ui/src/icon/hooks/__tests__/useIcons.test.tsx
similarity index 80%
rename from packages/twenty-ui/src/icon/hooks/__tests__/useIcons.test.ts
rename to packages/twenty-ui/src/icon/hooks/__tests__/useIcons.test.tsx
index 677878811c..17d9bb1c13 100644
--- a/packages/twenty-ui/src/icon/hooks/__tests__/useIcons.test.ts
+++ b/packages/twenty-ui/src/icon/hooks/__tests__/useIcons.test.tsx
@@ -1,11 +1,12 @@
import { renderHook } from '@testing-library/react';
-import { Provider } from 'jotai';
+import { type ReactNode } from 'react';
import {
Icon123,
IconBuildingSkyscraper,
IconUser,
} from '@ui/icon/components/TablerIcons';
+import { IconsContext } from '@ui/icon/internal/IconsContext';
import { useIcons } from '@ui/icon/hooks/useIcons';
const mockedStateIcons = {
@@ -14,14 +15,15 @@ const mockedStateIcons = {
IconBuildingSkyscraper,
};
-jest.mock('jotai', () => ({
- ...jest.requireActual('jotai'),
- useAtomValue: () => mockedStateIcons,
-}));
+const Wrapper = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+);
describe('useIcons', () => {
const { result } = renderHook(() => useIcons(), {
- wrapper: Provider,
+ wrapper: Wrapper,
});
it('returns default icon when no icon key is provided', () => {
diff --git a/packages/twenty-ui/src/icon/hooks/useIcons.ts b/packages/twenty-ui/src/icon/hooks/useIcons.ts
index 658c547c3d..e5c48bb86a 100644
--- a/packages/twenty-ui/src/icon/hooks/useIcons.ts
+++ b/packages/twenty-ui/src/icon/hooks/useIcons.ts
@@ -1,10 +1,10 @@
-import { useAtomValue } from 'jotai';
+import { useContext } from 'react';
import { Icon123 } from '@ui/icon/components/TablerIcons';
-import { iconsState } from '@ui/icon/states/iconsState';
+import { IconsContext } from '@ui/icon/internal/IconsContext';
export const useIcons = () => {
- const icons = useAtomValue(iconsState);
+ const icons = useContext(IconsContext);
const defaultIcon = Icon123;
const getIcons = () => {
diff --git a/packages/twenty-ui/src/icon/index.ts b/packages/twenty-ui/src/icon/index.ts
index d4bf4f960a..add4f94e68 100644
--- a/packages/twenty-ui/src/icon/index.ts
+++ b/packages/twenty-ui/src/icon/index.ts
@@ -489,5 +489,4 @@ export {
export { ThinkingOrbitLoaderIcon } from './components/ThinkingOrbitLoaderIcon';
export { useIcons } from './hooks/useIcons';
export { IconsProvider } from './providers/IconsProvider';
-export { iconsState } from './states/iconsState';
export type { IconComponentProps, IconComponent } from './types/IconComponent';
diff --git a/packages/twenty-ui/src/icon/internal/IconsContext.ts b/packages/twenty-ui/src/icon/internal/IconsContext.ts
new file mode 100644
index 0000000000..1711e84908
--- /dev/null
+++ b/packages/twenty-ui/src/icon/internal/IconsContext.ts
@@ -0,0 +1,5 @@
+import { createContext } from 'react';
+
+import { type IconComponent } from '@ui/icon/types/IconComponent';
+
+export const IconsContext = createContext>({});
diff --git a/packages/twenty-ui/src/icon/providers/IconsProvider.tsx b/packages/twenty-ui/src/icon/providers/IconsProvider.tsx
index aaa2c6e896..cf85d7df4e 100644
--- a/packages/twenty-ui/src/icon/providers/IconsProvider.tsx
+++ b/packages/twenty-ui/src/icon/providers/IconsProvider.tsx
@@ -1,20 +1,22 @@
-import { useSetAtom } from 'jotai';
-import { type JSX, useEffect } from 'react';
+import { type JSX, useEffect, useState } from 'react';
-import { iconsState } from '@ui/icon/states/iconsState';
+import { IconsContext } from '@ui/icon/internal/IconsContext';
+import { type IconComponent } from '@ui/icon/types/IconComponent';
type IconsProviderProps = {
children: JSX.Element;
};
export const IconsProvider = ({ children }: IconsProviderProps) => {
- const setIcons = useSetAtom(iconsState);
+ const [icons, setIcons] = useState>({});
useEffect(() => {
import('./internal/AllIcons').then(({ ALL_ICONS }) => {
setIcons(ALL_ICONS);
});
- }, [setIcons]);
+ }, []);
- return children;
+ return (
+ {children}
+ );
};
diff --git a/packages/twenty-ui/src/icon/states/iconsState.ts b/packages/twenty-ui/src/icon/states/iconsState.ts
deleted file mode 100644
index f4cbea163e..0000000000
--- a/packages/twenty-ui/src/icon/states/iconsState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { atom } from 'jotai';
-
-import { type IconComponent } from '@ui/icon/types/IconComponent';
-
-export const iconsState = atom>({});
-iconsState.debugLabel = 'iconsState';
diff --git a/packages/twenty-ui/src/input/TabButton/__stories__/TabButton.stories.tsx b/packages/twenty-ui/src/input/TabButton/__stories__/TabButton.stories.tsx
index 9d2634eb33..4697bea600 100644
--- a/packages/twenty-ui/src/input/TabButton/__stories__/TabButton.stories.tsx
+++ b/packages/twenty-ui/src/input/TabButton/__stories__/TabButton.stories.tsx
@@ -15,7 +15,6 @@ import {
CatalogDecorator,
type CatalogStory,
ComponentWithRouterDecorator,
- JotaiRootDecorator,
} from '@ui/testing';
import { type ReactNode } from 'react';
@@ -28,7 +27,7 @@ const TabContainer = ({ children }: { children?: ReactNode }) => {
const meta: Meta = {
title: 'UI/Input/Button/TabButton',
component: TabButton,
- decorators: [ComponentWithRouterDecorator, JotaiRootDecorator],
+ decorators: [ComponentWithRouterDecorator],
args: {
id: 'tab-button',
title: 'Tab Title',
diff --git a/packages/twenty-ui/src/navigation/MenuItemMultiSelectAvatar/__stories__/MenuItemMultiSelectAvatar.stories.tsx b/packages/twenty-ui/src/navigation/MenuItemMultiSelectAvatar/__stories__/MenuItemMultiSelectAvatar.stories.tsx
index 03493e574f..d70b633dd4 100644
--- a/packages/twenty-ui/src/navigation/MenuItemMultiSelectAvatar/__stories__/MenuItemMultiSelectAvatar.stories.tsx
+++ b/packages/twenty-ui/src/navigation/MenuItemMultiSelectAvatar/__stories__/MenuItemMultiSelectAvatar.stories.tsx
@@ -9,7 +9,6 @@ import {
type CatalogOptions,
type CatalogStory,
ComponentDecorator,
- JotaiRootDecorator,
} from '@ui/testing';
import { MenuItemMultiSelectAvatar } from '@ui/navigation/MenuItemMultiSelectAvatar/MenuItemMultiSelectAvatar';
@@ -29,7 +28,7 @@ export const Default: Story = {
contextualText: 'Contextual text',
avatar: ,
},
- decorators: [ComponentDecorator, JotaiRootDecorator],
+ decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory = {
@@ -84,5 +83,5 @@ export const Catalog: CatalogStory = {
} as CatalogOptions,
},
},
- decorators: [CatalogDecorator, JotaiRootDecorator],
+ decorators: [CatalogDecorator],
};
diff --git a/packages/twenty-ui/src/navigation/MenuItemSelectAvatar/__stories__/MenuItemSelectAvatar.stories.tsx b/packages/twenty-ui/src/navigation/MenuItemSelectAvatar/__stories__/MenuItemSelectAvatar.stories.tsx
index 8b6d79e083..22c33af7cd 100644
--- a/packages/twenty-ui/src/navigation/MenuItemSelectAvatar/__stories__/MenuItemSelectAvatar.stories.tsx
+++ b/packages/twenty-ui/src/navigation/MenuItemSelectAvatar/__stories__/MenuItemSelectAvatar.stories.tsx
@@ -9,7 +9,6 @@ import {
type CatalogOptions,
type CatalogStory,
ComponentDecorator,
- JotaiRootDecorator,
} from '@ui/testing';
import { MenuItemSelectAvatar } from '@ui/navigation/MenuItemSelectAvatar/MenuItemSelectAvatar';
@@ -38,7 +37,6 @@ export const Default: Story = {
),
ComponentDecorator,
- JotaiRootDecorator,
],
};
@@ -107,6 +105,5 @@ export const Catalog: CatalogStory = {
),
CatalogDecorator,
- JotaiRootDecorator,
],
};
diff --git a/packages/twenty-ui/src/testing/decorators/JotaiRootDecorator.tsx b/packages/twenty-ui/src/testing/decorators/JotaiRootDecorator.tsx
deleted file mode 100644
index b4ec98bce4..0000000000
--- a/packages/twenty-ui/src/testing/decorators/JotaiRootDecorator.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { type Decorator } from '@storybook/react-vite';
-
-import { Provider as JotaiProvider } from 'jotai';
-
-export const JotaiRootDecorator: Decorator = (Story) => (
-
-
-
-);
diff --git a/packages/twenty-ui/src/testing/index.ts b/packages/twenty-ui/src/testing/index.ts
index c188b460ee..5a4a3d4f74 100644
--- a/packages/twenty-ui/src/testing/index.ts
+++ b/packages/twenty-ui/src/testing/index.ts
@@ -21,7 +21,6 @@ export {
computeLocation,
ComponentWithRouterDecorator,
} from './decorators/ComponentWithRouterDecorator';
-export { JotaiRootDecorator } from './decorators/JotaiRootDecorator';
export { RouterDecorator } from './decorators/RouterDecorator';
export { AVATAR_URL_MOCK } from './mocks/avatarUrlMock';
export type { CatalogStory } from './types/CatalogStory';
diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts
index e5bbd31d1d..716cb77a0e 100644
--- a/packages/twenty-ui/src/utilities/index.ts
+++ b/packages/twenty-ui/src/utilities/index.ts
@@ -24,7 +24,6 @@ export { isNavigationModifierPressed } from './navigation/isNavigationModifierPr
export type { TriggerEventType } from './navigation/types/trigger-event.type';
export { useIsMobile } from './responsive/hooks/useIsMobile';
export { useScreenSize } from './screen-size/hooks/useScreenSize';
-export { createState } from './state/utils/createState';
export type { ClickOutsideAttributes } from './types/ClickOutsideAttributes';
export type { Nullable } from './types/Nullable';
export { getDisplayValueByUrlType } from './utils/getDisplayValueByUrlType';
diff --git a/packages/twenty-ui/src/utilities/state/utils/createState.ts b/packages/twenty-ui/src/utilities/state/utils/createState.ts
deleted file mode 100644
index 220424f24c..0000000000
--- a/packages/twenty-ui/src/utilities/state/utils/createState.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { atom, type PrimitiveAtom } from 'jotai';
-
-export const createState = ({
- key,
- defaultValue,
-}: {
- key: string;
- defaultValue: ValueType;
-}): PrimitiveAtom => {
- const jotaiAtom = atom(defaultValue);
- jotaiAtom.debugLabel = key;
- return jotaiAtom;
-};
diff --git a/yarn.lock b/yarn.lock
index 77ab916147..9c1aadfa46 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -54395,7 +54395,6 @@ __metadata:
glob: "npm:^11.1.0"
jest: "npm:29.7.0"
jest-environment-jsdom: "npm:30.0.0-beta.3"
- jotai: "npm:^2.17.1"
prettier: "npm:^3.1.1"
react: "npm:^19.2.0"
react-dom: "npm:^19.2.0"