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
@@ -20,7 +20,6 @@ import {
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import { renderHook } from '@testing-library/react';
import { iconsState } from 'twenty-ui/display';
import { SupportDriver } from '~/generated-metadata/graphql';
const redirectSpy = jest.fn();
@@ -159,7 +158,6 @@ describe('useAuth', () => {
const { result } = renderHook(
() => {
const client = useApolloClient();
const icons = useRecoilValue(iconsState);
const workspaceAuthProviders = useRecoilValue(
workspaceAuthProvidersState,
);
@@ -175,7 +173,6 @@ describe('useAuth', () => {
...useAuth(),
client,
state: {
icons,
workspaceAuthProviders,
billing,
isDeveloperDefaultSignInPrefilled,
@@ -200,7 +197,6 @@ describe('useAuth', () => {
const { state } = result.current;
expect(state.icons).toEqual({});
expect(state.workspaceAuthProviders).toEqual(null);
expect(state.billing).toBeNull();
expect(state.isDeveloperDefaultSignInPrefilled).toBe(false);
@@ -67,7 +67,6 @@ import { i18n } from '@lingui/core';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import { iconsState } from 'twenty-ui/display';
import { cookieStorage } from '~/utils/cookie-storage';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
@@ -132,7 +131,6 @@ export const useAuth = () => {
const emptySnapshot = snapshot_UNSTABLE();
const iconsValue = snapshot.getLoadable(iconsState).getValue();
const authProvidersValue = snapshot
.getLoadable(workspaceAuthProvidersState)
.getValue();
@@ -164,7 +162,6 @@ export const useAuth = () => {
.getValue();
const initialSnapshot = emptySnapshot.map(({ set }) => {
set(iconsState, iconsValue);
set(workspaceAuthProvidersState, authProvidersValue);
set(billingState, billing);
set(
@@ -1,4 +1,4 @@
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type AvailableWorkspaces } from '~/generated-metadata/graphql';
export const availableWorkspacesState = createState<AvailableWorkspaces>({
@@ -1,6 +1,6 @@
import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
export const billingCheckoutSessionState = createState<BillingCheckoutSession>({
key: 'billingCheckoutSessionState',
@@ -1,4 +1,4 @@
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type User } from '~/generated-metadata/graphql';
export type CurrentUser = Pick<
@@ -1,5 +1,5 @@
import { type ObjectPermissions } from 'twenty-shared/types';
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type UserWorkspace } from '~/generated-metadata/graphql';
export type CurrentUserWorkspace = Pick<
@@ -1,4 +1,4 @@
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type DeletedWorkspaceMember } from '~/generated-metadata/graphql';
export const currentWorkspaceDeletedMembersState = createState<
@@ -1,5 +1,5 @@
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
export type CurrentWorkspaceMember = Omit<
WorkspaceMember,
@@ -1,5 +1,5 @@
import { type PartialWorkspaceMember } from '@/settings/roles/types/RoleWithPartialMembers';
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
export const currentWorkspaceMembersState = createState<
PartialWorkspaceMember[]
@@ -1,4 +1,4 @@
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
import {
type Application,
type Role,
@@ -1,4 +1,4 @@
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
export const isCurrentUserLoadedState = createState<boolean>({
key: 'isCurrentUserLoadedState',
defaultValue: false,
@@ -1,4 +1,4 @@
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type AuthToken } from '~/generated-metadata/graphql';
export const loginTokenState = createState<AuthToken['token'] | null>({
@@ -1,4 +1,4 @@
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
export const previousUrlState = createState<string>({
key: 'previousUrlState',
defaultValue: '',
@@ -1,4 +1,4 @@
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
export const qrCodeState = createState<string | null>({
key: 'qrCodeState',
@@ -1,5 +1,5 @@
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
export const signInUpModeState = createState<SignInUpMode>({
key: 'signInUpModeState',
@@ -1,4 +1,4 @@
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
export enum SignInUpStep {
Init = 'init',
Email = 'email',
@@ -1,4 +1,4 @@
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type AuthTokenPair } from '~/generated-metadata/graphql';
import { cookieStorageEffect } from '~/utils/recoil/cookieStorageEffect';
@@ -1,5 +1,5 @@
import { type PublicWorkspaceDataOutput } from '~/generated-metadata/graphql';
import { createState } from 'twenty-ui/utilities';
import { createState } from '@/ui/utilities/state/utils/createState';
export const workspacePublicDataState =
createState<PublicWorkspaceDataOutput | null>({