Remove recoil sync (#11569)

Recoil-sync was causing issues with Firefox, replacing it with a simpler
mechanism to hydrate variables on page load

---------

Co-authored-by: etiennejouan <jouan.etienne@gmail.com>
This commit is contained in:
Félix Malfait
2025-04-15 13:32:12 +02:00
committed by GitHub
parent 6c2d64dcb2
commit e8db0176a1
17 changed files with 102 additions and 139 deletions

View File

@ -54,7 +54,6 @@
"buffer": "^6.0.3",
"docx": "^9.1.0",
"file-saver": "^2.0.5",
"recoil-sync": "^0.2.0",
"transliteration": "^2.3.5",
"twenty-shared": "workspace:*",
"twenty-ui": "workspace:*"

View File

@ -9,16 +9,14 @@ import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { HelmetProvider } from 'react-helmet-async';
import { RecoilRoot } from 'recoil';
import { RecoilURLSyncJSON } from 'recoil-sync';
import { initialI18nActivate } from '~/utils/i18n/initialI18nActivate';
import { IconsProvider } from 'twenty-ui/display';
import { initialI18nActivate } from '~/utils/i18n/initialI18nActivate';
initialI18nActivate();
export const App = () => {
return (
<RecoilRoot>
<RecoilURLSyncJSON location={{ part: 'queryParams' }}>
<AppErrorBoundary
resetOnLocationChange={false}
FallbackComponent={AppRootErrorFallback}
@ -37,7 +35,6 @@ export const App = () => {
</SnackBarProviderScope>
</I18nProvider>
</AppErrorBoundary>
</RecoilURLSyncJSON>
</RecoilRoot>
);
};

View File

@ -26,6 +26,7 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
import { isDefined } from 'twenty-shared/utils';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState';
// TODO: break down into smaller functions and / or hooks
// - moved usePageChangeEffectNavigateLocation into dedicated hook
@ -44,6 +45,8 @@ export const PageChangeEffect = () => {
const eventTracker = useEventTracker();
const { initializeQueryParamState } = useInitializeQueryParamState();
//TODO: refactor useResetTableRowSelection hook to not throw when the argument `recordTableId` is an empty string
// - replace CoreObjectNamePlural.Person
const objectNamePlural =
@ -64,10 +67,12 @@ export const PageChangeEffect = () => {
}, [location, previousLocation, executeTasksOnAnyLocationChange]);
useEffect(() => {
initializeQueryParamState();
if (isDefined(pageChangeEffectNavigateLocation)) {
navigate(pageChangeEffectNavigateLocation);
}
}, [navigate, pageChangeEffectNavigateLocation]);
}, [navigate, pageChangeEffectNavigateLocation, initializeQueryParamState]);
useEffect(() => {
const isLeavingRecordIndexPage = !!matchPath(

View File

@ -0,0 +1,66 @@
import { useRecoilCallback } from 'recoil';
import { isQueryParamInitializedState } from '@/app/states/isQueryParamInitializedState';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
// Initialize state that are hydrated from query parameters
// We used to use recoil-sync to do this, but it was causing issues with Firefox
export const useInitializeQueryParamState = () => {
const initializeQueryParamState = useRecoilCallback(
({ set, snapshot }) =>
() => {
const isInitialized = snapshot
.getLoadable(isQueryParamInitializedState)
.getValue();
if (!isInitialized) {
const handlers = {
billingCheckoutSession: (value: string) => {
try {
const parsedValue = JSON.parse(decodeURIComponent(value));
if (
typeof parsedValue === 'object' &&
parsedValue !== null &&
'plan' in parsedValue &&
'interval' in parsedValue &&
'requirePaymentMethod' in parsedValue
) {
set(
billingCheckoutSessionState,
parsedValue as BillingCheckoutSession,
);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(
'Failed to parse billingCheckoutSession from URL',
error,
);
set(
billingCheckoutSessionState,
BILLING_CHECKOUT_SESSION_DEFAULT_VALUE,
);
}
},
};
const queryParams = new URLSearchParams(window.location.search);
for (const [paramName, handler] of Object.entries(handlers)) {
const value = queryParams.get(paramName);
if (value !== null) {
handler(value);
}
}
set(isQueryParamInitializedState, true);
}
},
[],
);
return { initializeQueryParamState };
};

View File

@ -0,0 +1,6 @@
import { createState } from 'twenty-ui/utilities';
export const isQueryParamInitializedState = createState<boolean>({
key: 'isQueryParamInitializedState',
defaultValue: false,
});

View File

@ -10,18 +10,10 @@ const StyledContent = styled.div`
type AuthModalProps = {
children: React.ReactNode;
isOpenAnimated?: boolean;
};
export const AuthModal = ({
children,
isOpenAnimated = true,
}: AuthModalProps) => (
<Modal
padding={'none'}
modalVariant="primary"
isOpenAnimated={isOpenAnimated}
>
export const AuthModal = ({ children }: AuthModalProps) => (
<Modal padding={'none'} modalVariant="primary">
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
<StyledContent>{children}</StyledContent>
</ScrollWrapper>

View File

@ -4,12 +4,10 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
import { animateModalState } from '@/auth/states/animateModalState';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { EmailVerificationSent } from '../sign-in-up/components/EmailVerificationSent';
@ -21,7 +19,6 @@ export const VerifyEmailEffect = () => {
const [searchParams] = useSearchParams();
const [isError, setIsError] = useState(false);
const setAnimateModal = useSetRecoilState(animateModalState);
const email = searchParams.get('email');
const emailVerificationToken = searchParams.get('emailVerificationToken');
@ -52,7 +49,6 @@ export const VerifyEmailEffect = () => {
const workspaceUrl = getWorkspaceUrl(workspaceUrls);
if (workspaceUrl.slice(0, -1) !== window.location.origin) {
setAnimateModal(false);
return await redirectToWorkspaceDomain(workspaceUrl, AppPath.Verify, {
loginToken: loginToken.token,
});

View File

@ -41,7 +41,6 @@ import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTi
import { currentUserState } from '../states/currentUserState';
import { tokenPairState } from '../states/tokenPairState';
import { animateModalState } from '@/auth/states/animateModalState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import {
SignInUpStep,
@ -115,7 +114,6 @@ export const useAuth = () => {
const goToRecoilSnapshot = useGotoRecoilSnapshot();
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
const setAnimateModal = useSetRecoilState(animateModalState);
const [, setSearchParams] = useSearchParams();
@ -422,7 +420,6 @@ export const useAuth = () => {
}
if (isMultiWorkspaceEnabled) {
setAnimateModal(false);
return await redirectToWorkspaceDomain(
getWorkspaceUrl(signUpResult.data.signUp.workspace.workspaceUrls),
isEmailVerificationRequired ? AppPath.SignInUp : AppPath.Verify,
@ -446,7 +443,6 @@ export const useAuth = () => {
handleGetAuthTokensFromLoginToken,
setSignInUpStep,
setSearchParams,
setAnimateModal,
isEmailVerificationRequired,
redirectToWorkspaceDomain,
],

View File

@ -1,26 +0,0 @@
import { urlSyncEffect } from 'recoil-sync';
import { createState } from 'twenty-ui/utilities';
export const animateModalState = createState<boolean>({
key: 'animateModalState',
defaultValue: true,
effects: [
urlSyncEffect({
itemKey: 'animateModal',
refine: (value: unknown) => {
if (typeof value === 'boolean') {
return {
type: 'success',
value: value as boolean,
warnings: [],
} as const;
}
return {
type: 'failure',
message: 'Invalid animateModalState',
path: [] as any,
} as const;
},
}),
],
});

View File

@ -1,34 +1,8 @@
import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { syncEffect } from 'recoil-sync';
import { createState } from 'twenty-ui/utilities';
export const billingCheckoutSessionState = createState<BillingCheckoutSession>({
key: 'billingCheckoutSessionState',
defaultValue: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE,
effects: [
syncEffect({
itemKey: 'billingCheckoutSession',
refine: (value: unknown) => {
if (
typeof value === 'object' &&
value !== null &&
'plan' in value &&
'interval' in value &&
'requirePaymentMethod' in value
) {
return {
type: 'success',
value: value as BillingCheckoutSession,
warnings: [],
} as const;
}
return {
type: 'failure',
message: 'Invalid BillingCheckoutSessionState',
path: [] as any,
} as const;
},
}),
],
});

View File

@ -1,4 +1,3 @@
import { animateModalState } from '@/auth/states/animateModalState';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { useRecoilCallback } from 'recoil';
@ -7,7 +6,6 @@ export const useBuildSearchParamsFromUrlSyncedStates = () => {
const buildSearchParamsFromUrlSyncedStates = useRecoilCallback(
({ snapshot }) =>
async () => {
const animateModal = snapshot.getLoadable(animateModalState).getValue();
const billingCheckoutSession = snapshot
.getLoadable(billingCheckoutSessionState)
.getValue();
@ -18,7 +16,6 @@ export const useBuildSearchParamsFromUrlSyncedStates = () => {
billingCheckoutSession: JSON.stringify(billingCheckoutSession),
}
: {}),
...(animateModal === false ? { animateModal: 'false' } : {}),
};
return output;

View File

@ -1,19 +1,15 @@
import { animateModalState } from '@/auth/states/animateModalState';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { AppPath } from '@/types/AppPath';
import { useSetRecoilState } from 'recoil';
import { WorkspaceUrls } from '~/generated/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
export const useImpersonationRedirect = () => {
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const setAnimateModal = useSetRecoilState(animateModalState);
const executeImpersonationRedirect = async (
workspaceUrls: WorkspaceUrls,
loginToken: string,
) => {
setAnimateModal(false);
return await redirectToWorkspaceDomain(
getWorkspaceUrl(workspaceUrls),
AppPath.Verify,

View File

@ -174,7 +174,6 @@ export type ModalProps = React.PropsWithChildren & {
className?: string;
hotkeyScope?: ModalHotkeyScope;
onEnter?: () => void;
isOpenAnimated?: boolean;
modalVariant?: ModalVariants;
} & (
| { isClosable: true; onClose: () => void }
@ -197,7 +196,6 @@ export const Modal = ({
isClosable = false,
onClose,
modalVariant = 'primary',
isOpenAnimated = true,
}: ModalProps) => {
const isMobile = useIsMobile();
const modalRef = useRef<HTMLDivElement>(null);
@ -263,7 +261,7 @@ export const Modal = ({
ref={modalRef}
size={size}
padding={padding}
initial={isOpenAnimated ? 'hidden' : 'visible'}
initial="hidden"
animate="visible"
exit="exit"
layout

View File

@ -1,5 +1,4 @@
import { AuthModal } from '@/auth/components/AuthModal';
import { animateModalState } from '@/auth/states/animateModalState';
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
@ -19,7 +18,6 @@ import { Global, css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
import { Outlet } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useScreenSize } from 'twenty-ui/utilities';
const StyledLayout = styled.div`
@ -65,7 +63,6 @@ export const DefaultLayout = () => {
const windowsWidth = useScreenSize().width;
const showAuthModal = useShowAuthModal();
const useShowFullScreen = useShowFullscreen();
const animateModal = useRecoilValue(animateModalState);
return (
<>
@ -111,7 +108,7 @@ export const DefaultLayout = () => {
</StyledMainContainer>
<AnimatePresence mode="wait">
<LayoutGroup>
<AuthModal isOpenAnimated={animateModal}>
<AuthModal>
<Outlet />
</AuthModal>
</LayoutGroup>

View File

@ -1,7 +1,6 @@
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { useAuth } from '@/auth/hooks/useAuth';
import { animateModalState } from '@/auth/states/animateModalState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
@ -67,7 +66,6 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
const setMultiWorkspaceDropdownState = useSetRecoilState(
multiWorkspaceDropdownState,
);
const setAnimateModal = useSetRecoilState(animateModalState);
const handleChange = async (workspace: Workspaces[0]) => {
redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
@ -76,7 +74,6 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
const createWorkspace = () => {
signUpInNewWorkspaceMutation({
onCompleted: async (data) => {
setAnimateModal(false);
return await redirectToWorkspaceDomain(
getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls),
AppPath.Verify,

View File

@ -172,7 +172,7 @@ export const ChooseYourPlan = () => {
?.baseProduct.name;
return (
<Modal.Content>
<Modal.Content isVerticalCentered>
{isDefined(baseProductPrice) && isDefined(billing) ? (
<>
<Title noMarginTop>

View File

@ -16495,13 +16495,6 @@ __metadata:
languageName: node
linkType: hard
"@recoiljs/refine@npm:^0.1.1":
version: 0.1.1
resolution: "@recoiljs/refine@npm:0.1.1"
checksum: 10c0/27ca1c4ea500b1b99a3af5ee48d6749310e5138e83b87ddfb41304e2222fa64567acb985f340334ab73980202ab277a0f133c40817fbec786076c06bfb3f5363
languageName: node
linkType: hard
"@redis/bloom@npm:1.2.0, @redis/bloom@npm:^1.2.0":
version: 1.2.0
resolution: "@redis/bloom@npm:1.2.0"
@ -50073,18 +50066,6 @@ __metadata:
languageName: node
linkType: hard
"recoil-sync@npm:^0.2.0":
version: 0.2.0
resolution: "recoil-sync@npm:0.2.0"
dependencies:
"@recoiljs/refine": "npm:^0.1.1"
transit-js: "npm:^0.8.874"
peerDependencies:
recoil: ">=0.7.3"
checksum: 10c0/f3a671a3cfcecadb5bbb22d47b3b040721be1013ee91d4fa0570a64ca1707eb68e818b291a82e133551adf4a2dd2fc9a0715d550c1f1e3db82f3b8d9169e2a5a
languageName: node
linkType: hard
"recoil@npm:^0.7.7":
version: 0.7.7
resolution: "recoil@npm:0.7.7"
@ -54388,13 +54369,6 @@ __metadata:
languageName: node
linkType: hard
"transit-js@npm:^0.8.874":
version: 0.8.874
resolution: "transit-js@npm:0.8.874"
checksum: 10c0/6ca0b413f1e3780a4a56b9bbde54b67a2ffefda1c052f8dc68bf28c1a3df4c29baa6f17f60484c9f2fb056a8c7dcd1fe04d24e091fb99afe1525e4d4406ad58a
languageName: node
linkType: hard
"transliteration@npm:^2.3.5":
version: 2.3.5
resolution: "transliteration@npm:2.3.5"
@ -55005,7 +54979,6 @@ __metadata:
eslint-plugin-unused-imports: "npm:^3.0.0"
file-saver: "npm:^2.0.5"
optionator: "npm:^0.9.1"
recoil-sync: "npm:^0.2.0"
transliteration: "npm:^2.3.5"
twenty-shared: "workspace:*"
twenty-ui: "workspace:*"