Migrate twenty ui to jotai (#17900)

## Remove Recoil from twenty-ui

Completely removes the `recoil` dependency from `twenty-ui` by
converting all atoms, hooks, and providers to Jotai equivalents.

### twenty-ui
- `createState` now returns a Jotai `PrimitiveAtom` instead of a Recoil
atom
- `iconsState`, `IconsProvider`, `useIcons` converted to Jotai
(`useSetAtom`, `useAtomValue`)
- `RecoilRootDecorator` now uses Jotai `Provider` (name kept for compat)
- Deleted unused `invalidAvatarUrlsState` (Avatar already uses
`invalidAvatarUrlsAtomV2`)
- Removed `recoil` from `package.json`

### twenty-front
- Created local Recoil `createState` at
`@/ui/utilities/state/utils/createState` for ~112 state files still on
Recoil
- Updated all imports accordingly
- Removed `iconsState` from Recoil snapshot preservation in `useAuth`
(lives in Jotai store now)
This commit is contained in:
Charles Bochet
2026-02-12 16:43:34 +01:00
committed by GitHub
parent 09e48addb2
commit 310d13fd17
139 changed files with 165 additions and 168 deletions
@@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const invalidAvatarUrlsState = atom<string[]>({
key: 'invalidAvatarUrlsState',
default: [],
});
@@ -1,5 +1,5 @@
import { renderHook } from '@testing-library/react';
import * as recoil from 'recoil';
import * as jotai from 'jotai';
import {
Icon123,
@@ -15,10 +15,10 @@ describe('useIcons', () => {
IconBuildingSkyscraper,
};
jest
.spyOn(recoil, 'useRecoilValue')
.spyOn(jotai, 'useAtomValue')
.mockImplementationOnce(() => mockedStateIcons);
const { result } = renderHook(() => useIcons(), {
wrapper: recoil.RecoilRoot,
wrapper: jotai.Provider,
});
it('returns default icon when no icon key is provided', () => {
@@ -1,10 +1,10 @@
import { useRecoilValue } from 'recoil';
import { useAtomValue } from 'jotai';
import { Icon123 } from '@ui/display/icon/components/TablerIcons';
import { iconsState } from '@ui/display/icon/states/iconsState';
export const useIcons = () => {
const icons = useRecoilValue(iconsState);
const icons = useAtomValue(iconsState);
const defaultIcon = Icon123;
const getIcons = () => {
@@ -1,5 +1,5 @@
import { useSetAtom } from 'jotai';
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import { iconsState } from '@ui/display/icon/states/iconsState';
@@ -8,7 +8,7 @@ export type IconsProviderProps = {
};
export const IconsProvider = ({ children }: IconsProviderProps) => {
const setIcons = useSetRecoilState(iconsState);
const setIcons = useSetAtom(iconsState);
useEffect(() => {
import('./internal/AllIcons').then(({ ALL_ICONS }) => {
@@ -1,7 +1,6 @@
import { type IconComponent } from '@ui/display/icon/types/IconComponent';
import { createState } from '@ui/utilities/state/utils/createState';
import { atom } from 'jotai';
export const iconsState = createState<Record<string, IconComponent>>({
key: 'iconsState',
defaultValue: {},
});
import { type IconComponent } from '@ui/display/icon/types/IconComponent';
export const iconsState = atom<Record<string, IconComponent>>({});
iconsState.debugLabel = 'iconsState';
-1
View File
@@ -12,7 +12,6 @@ export { Avatar } from './avatar/components/Avatar';
export type { AvatarGroupProps } from './avatar/components/AvatarGroup';
export { AvatarGroup } from './avatar/components/AvatarGroup';
export { invalidAvatarUrlsAtomV2 } from './avatar/components/states/invalidAvatarUrlsAtomV2';
export { invalidAvatarUrlsState } from './avatar/components/states/isInvalidAvatarUrlState';
export { AVATAR_PROPERTIES_BY_SIZE } from './avatar/constants/AvatarPropertiesBySize';
export type { AvatarSize } from './avatar/types/AvatarSize';
export type { AvatarType } from './avatar/types/AvatarType';
@@ -1,9 +1,10 @@
import { type Decorator } from '@storybook/react-vite';
import { RecoilRoot } from 'recoil';
import { Provider as JotaiProvider } from 'jotai';
// Kept as RecoilRootDecorator name for backward compatibility during migration
export const RecoilRootDecorator: Decorator = (Story, _context) => (
<RecoilRoot>
<JotaiProvider>
<Story />
</RecoilRoot>
</JotaiProvider>
);
@@ -1,18 +1,13 @@
import { atom, type AtomEffect } from 'recoil';
import { atom, type PrimitiveAtom } from 'jotai';
export const createState = <ValueType>({
key,
defaultValue,
effects,
}: {
key: string;
defaultValue: ValueType;
effects?: ReadonlyArray<AtomEffect<ValueType>>;
}) => {
const recoilState = atom<ValueType>({
key,
default: defaultValue,
effects,
});
return recoilState;
}): PrimitiveAtom<ValueType> => {
const jotaiAtom = atom<ValueType>(defaultValue);
jotaiAtom.debugLabel = key;
return jotaiAtom;
};