RawLayout.tsx 2.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import type { JSX, ReactNode } from 'react';
  2. import React, { useState } from 'react';
  3. import dynamic from 'next/dynamic';
  4. import Head from 'next/head';
  5. import type { ColorScheme } from '@growi/core';
  6. import { useIsomorphicLayoutEffect } from 'usehooks-ts';
  7. import {
  8. NextThemesProvider,
  9. useNextThemes,
  10. } from '~/stores-universal/use-next-themes';
  11. import loggerFactory from '~/utils/logger';
  12. import styles from './RawLayout.module.scss';
  13. const toastContainerClass = styles['grw-toast-container'] ?? '';
  14. const logger = loggerFactory('growi:cli:RawLayout');
  15. const ToastContainer = dynamic(
  16. () => import('react-toastify').then((mod) => mod.ToastContainer),
  17. { ssr: false },
  18. );
  19. type Props = {
  20. className?: string;
  21. children?: ReactNode;
  22. };
  23. export const RawLayout = ({ children, className }: Props): JSX.Element => {
  24. const classNames: string[] = ['layout-root', 'growi'];
  25. // Use state to handle SSR/CSR className mismatch
  26. // Using state ensures React properly updates the DOM after hydration
  27. const [dynamicClassName, setDynamicClassName] = useState<string | undefined>(
  28. undefined,
  29. );
  30. useIsomorphicLayoutEffect(() => {
  31. if (className !== dynamicClassName) {
  32. setDynamicClassName(className);
  33. }
  34. }, [className, dynamicClassName]);
  35. if (dynamicClassName != null) {
  36. classNames.push(dynamicClassName);
  37. }
  38. // get color scheme from next-themes
  39. const { resolvedTheme, resolvedThemeByAttributes } = useNextThemes();
  40. const [colorScheme, setColorScheme] = useState<ColorScheme | undefined>(
  41. undefined,
  42. );
  43. // set colorScheme in CSR
  44. useIsomorphicLayoutEffect(() => {
  45. setColorScheme(resolvedTheme ?? resolvedThemeByAttributes);
  46. }, [resolvedTheme, resolvedThemeByAttributes]);
  47. return (
  48. <>
  49. <Head>
  50. <meta charSet="utf-8" />
  51. <meta name="viewport" content="initial-scale=1.0, width=device-width" />
  52. </Head>
  53. <NextThemesProvider>
  54. <div className={classNames.join(' ')}>
  55. {children}
  56. <ToastContainer className={toastContainerClass} theme={colorScheme} />
  57. </div>
  58. </NextThemesProvider>
  59. </>
  60. );
  61. };