🐛(front) resolve React hydration errors

resolve React hydration errors and infinite update loop

🐛(front) resolve React hydration errors

resolve React hydration errors and infinite update loop
This commit is contained in:
Eléonore Voisin
2025-12-02 10:35:48 +01:00
parent d3922b7448
commit 83904d8878
7 changed files with 83 additions and 100 deletions
+1 -1
View File
@@ -14,9 +14,9 @@ and this project adheres to
### Fixed
- 🐛(front) resolve React hydration errors
- 🚑️(user) allow longer short names #182
## [0.0.9] - 2025-11-17
### Added
+1 -1
View File
@@ -36,6 +36,7 @@
"i18next-browser-languagedetector": "8.1.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"lottie-react": "^2.4.1",
"luxon": "3.6.1",
"micromark-extension-llm-math": "3.1.1-20250610",
"next": "15.3.3",
@@ -45,7 +46,6 @@
"react-dom": "18.3.1",
"react-i18next": "15.5.2",
"react-intersection-observer": "9.16.0",
"react-lottie": "^1.2.10",
"react-markdown": "10.1.0",
"react-select": "5.10.1",
"rehype-katex": "7.0.1",
@@ -1,4 +1,4 @@
import { PropsWithChildren, useRef, useState } from 'react';
import { PropsWithChildren, useEffect, useRef, useState } from 'react';
import { css } from 'styled-components';
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
@@ -35,8 +35,16 @@ export const DropdownMenu = ({
}: PropsWithChildren<DropdownMenuProps>) => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const [isOpen, setIsOpen] = useState(false);
const [buttonWidth, setButtonWidth] = useState<number | undefined>(undefined);
const blockButtonRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Mettre à jour la largeur uniquement côté client
if (blockButtonRef.current) {
setButtonWidth(blockButtonRef.current.clientWidth);
}
}, [isOpen]);
const onOpenChange = (isOpen: boolean) => {
setIsOpen(isOpen);
};
@@ -76,7 +84,7 @@ export const DropdownMenu = ({
>
<Box
$maxWidth="320px"
$minWidth={`${blockButtonRef.current?.clientWidth}px`}
$minWidth={buttonWidth ? `${buttonWidth}px` : undefined}
role="menu"
>
{topMessage && (
@@ -1,20 +1,17 @@
import Lottie from 'react-lottie';
import dynamic from 'next/dynamic';
const Lottie = dynamic(() => import('lottie-react'), { ssr: false });
import searchingAnimation from '@/assets/lotties/searching';
export const Loader = () => {
const LoaderOptions = {
loop: true,
autoplay: true,
animationData: searchingAnimation,
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice',
},
} as const;
export function Loader() {
return (
<div>
<Lottie options={LoaderOptions} height={24} width={24} />
<div role="status">
<Lottie
animationData={searchingAnimation}
loop
autoplay
style={{ width: 24, height: 24 }}
/>
</div>
);
};
}
@@ -3,6 +3,7 @@ import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { createPortal } from 'react-dom';
@@ -47,6 +48,11 @@ interface ToastProviderProps {
export const ToastProvider = ({ children }: ToastProviderProps) => {
const [toasts, setToasts] = useState<ToastItem[]>([]);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
const showToast = useCallback(
(
@@ -83,7 +89,9 @@ export const ToastProvider = ({ children }: ToastProviderProps) => {
return (
<ToastContext.Provider value={value}>
{children}
{typeof window !== 'undefined' &&
{isMounted &&
typeof document !== 'undefined' &&
document.body &&
createPortal(
<Box
aria-live="polite"
@@ -1,74 +1,64 @@
import { CunninghamProvider } from '@openfun/cunningham-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import dynamic from 'next/dynamic';
import { useEffect } from 'react';
import { ToastProvider } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
import { useResponsiveStore } from '@/stores/';
import { useResponsiveStore } from '@/stores';
import { ConfigProvider } from './config/';
import { ConfigProvider } from './config';
// Client-only providers
const ToastProviderNoSSR = dynamic(
() => import('@/components').then((mod) => ({ default: mod.ToastProvider })),
{ ssr: false, loading: () => null },
);
const CunninghamProviderNoSSR = dynamic(
() =>
import('@openfun/cunningham-react').then((mod) => ({
default: mod.CunninghamProvider,
})),
{ ssr: false },
);
/**
* QueryClient:
* - defaultOptions:
* - staleTime:
* - global cache duration - we decided 3 minutes
* - It can be overridden to each query
*/
const defaultOptions = {
queries: {
staleTime: 1000 * 60 * 3,
retry: 1,
},
};
const queryClient = new QueryClient({
defaultOptions,
defaultOptions: {
queries: { staleTime: 1000 * 60 * 3, retry: 1 },
mutations: {
onError: (error) => {
if (
error instanceof Error &&
'status' in error &&
error.status === 401
) {
void queryClient.resetQueries({ queryKey: [KEY_AUTH] });
setAuthUrl();
if (typeof window !== 'undefined') {
window.location.href = '/401';
}
}
},
},
},
});
export function AppProvider({ children }: { children: React.ReactNode }) {
const { theme } = useCunninghamTheme();
const { replace } = useRouter();
const initializeResizeListener = useResponsiveStore(
(state) => state.initializeResizeListener,
);
const theme = useCunninghamTheme((state) => state.theme);
useEffect(() => {
return initializeResizeListener();
}, [initializeResizeListener]);
useEffect(() => {
queryClient.setDefaultOptions({
...defaultOptions,
mutations: {
onError: (error) => {
if (
error instanceof Error &&
'status' in error &&
error.status === 401
) {
void queryClient.resetQueries({
queryKey: [KEY_AUTH],
});
setAuthUrl();
void replace(`/401`);
}
},
},
});
}, [replace]);
return useResponsiveStore.getState().initializeResizeListener();
}, []);
return (
<QueryClientProvider client={queryClient}>
<CunninghamProvider theme={theme}>
<CunninghamProviderNoSSR theme={theme}>
<ConfigProvider>
<ToastProvider>
<ToastProviderNoSSR>
<Auth>{children}</Auth>
</ToastProvider>
</ToastProviderNoSSR>
</ConfigProvider>
</CunninghamProvider>
</CunninghamProviderNoSSR>
</QueryClientProvider>
);
}
+9 -29
View File
@@ -6451,14 +6451,6 @@ babel-preset-jest@^29.6.3:
babel-plugin-jest-hoist "^29.6.3"
babel-preset-current-node-syntax "^1.0.0"
babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
bail@^2.0.0:
version "2.0.2"
resolved "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz"
@@ -7004,11 +6996,6 @@ core-js-compat@^3.40.0:
dependencies:
browserslist "^4.24.4"
core-js@^2.4.0:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.0.0, core-js@^3.38.1:
version "3.42.0"
resolved "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz"
@@ -10097,7 +10084,14 @@ loose-envify@^1.0.0, loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lottie-web@^5.12.2:
lottie-react@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/lottie-react/-/lottie-react-2.4.1.tgz#4bd3f2a8a5e48edbd43c05ca5080fdd50f049d31"
integrity sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw==
dependencies:
lottie-web "^5.10.2"
lottie-web@^5.10.2:
version "5.13.0"
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.13.0.tgz#441d3df217cc8ba302338c3f168e1a3af0f221d3"
integrity sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==
@@ -11428,7 +11422,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -11825,15 +11819,6 @@ react-lifecycles-compat@^3.0.0:
resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-lottie@^1.2.10:
version "1.2.10"
resolved "https://registry.yarnpkg.com/react-lottie/-/react-lottie-1.2.10.tgz#399f78a448a7833b2380d74fc489ecf15f8d18c7"
integrity sha512-x0eWX3Z6zSx1XM5QSjnLupc6D22LlMCB0PH06O/N/epR2hsLaj1Vxd9RtMnbbEHjJ/qlsgHJ6bpN3vnZI92hjw==
dependencies:
babel-runtime "^6.26.0"
lottie-web "^5.12.2"
prop-types "^15.6.1"
[email protected]:
version "10.1.0"
resolved "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz"
@@ -12101,11 +12086,6 @@ regenerate@^1.4.2:
resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regex-recursion@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/regex-recursion/-/regex-recursion-6.0.2.tgz#a0b1977a74c87f073377b938dbedfab2ea582b33"