diff --git a/packages/react/src/components/NoSsr/NoSsr.api.mdx b/packages/react/src/components/NoSsr/NoSsr.api.mdx new file mode 100644 index 000000000..fa2564951 --- /dev/null +++ b/packages/react/src/components/NoSsr/NoSsr.api.mdx @@ -0,0 +1,18 @@ +import { Meta } from '@storybook/addon-docs/blocks'; +import { TableInterface } from '~storybook/components/TableInterface'; + + + +# NoSsr API + +```js +import { NoSsr } from '@esfront/react'; +``` + +## Component name + +The name `ESNoSsr` can be used when providing default props. + +## Props + + diff --git a/packages/react/src/components/NoSsr/NoSsr.stories.tsx b/packages/react/src/components/NoSsr/NoSsr.stories.tsx new file mode 100644 index 000000000..66ddd6d8e --- /dev/null +++ b/packages/react/src/components/NoSsr/NoSsr.stories.tsx @@ -0,0 +1,48 @@ +import { Meta, StoryContext, StoryObj } from '@storybook/react-vite'; + +import { NoSsr } from './NoSsr'; + +const getFallbackText = (context: StoryContext) => { + return context.globals.locale === 'ru' ? 'Содержимое с сервера' : 'Content from the server'; +}; + +const getChildrenText = (context: StoryContext) => { + return context.globals.locale === 'ru' ? 'Содержимое из браузера' : 'Content from the browser'; +}; + +const meta: Meta = { + tags: ['autodocs'], + component: NoSsr, + parameters: { + references: ['NoSsr'], + }, + argTypes: { + children: { + table: { + disable: true, + }, + }, + fallback: { + table: { + disable: true, + }, + }, + }, + args: { + defer: false, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Demo: Story = { + render: function Render(args, context) { + return ( + {getFallbackText(context)}}> +
{getChildrenText(context)}
+
+ ); + }, +}; diff --git a/packages/react/src/components/NoSsr/NoSsr.tsx b/packages/react/src/components/NoSsr/NoSsr.tsx new file mode 100644 index 000000000..84297fdf3 --- /dev/null +++ b/packages/react/src/components/NoSsr/NoSsr.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import { NoSsrProps } from './NoSsr.types'; + +import { useEnhancedEffect } from '../../hooks'; +import { useDefaultProps } from '../../theming'; + +/** + * Removes its children from server-side rendering, so they are only rendered in the browser. Use it for the content + * that depends on browser APIs or that would otherwise cause a hydration mismatch. + */ +export const NoSsr = (inProps: NoSsrProps) => { + const { + children, + defer = false, + fallback = null, + } = useDefaultProps({ + props: inProps, + name: 'ESNoSsr', + }); + + const [isMounted, setMounted] = useState(false); + + useEnhancedEffect(() => { + if (!defer) { + setMounted(true); + } + }, [defer]); + + useEffect(() => { + if (defer) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true); + } + }, [defer]); + + return isMounted ? children : fallback; +}; diff --git a/packages/react/src/components/NoSsr/NoSsr.types.ts b/packages/react/src/components/NoSsr/NoSsr.types.ts new file mode 100644 index 000000000..c4d88fb6d --- /dev/null +++ b/packages/react/src/components/NoSsr/NoSsr.types.ts @@ -0,0 +1,16 @@ +import { ReactNode } from 'react'; + +export interface NoSsrProps { + children?: ReactNode; + /** + * If `true`, the children are additionally deferred to a different screen frame. Use it to unblock the initial paint + * when the children are expensive to render. + * @default false + */ + defer?: boolean; + /** + * The content rendered on the server and until the children are mounted on the client. + * @default null + */ + fallback?: ReactNode; +} diff --git a/packages/react/src/components/NoSsr/index.ts b/packages/react/src/components/NoSsr/index.ts new file mode 100644 index 000000000..435eb7b15 --- /dev/null +++ b/packages/react/src/components/NoSsr/index.ts @@ -0,0 +1,2 @@ +export { NoSsr } from './NoSsr'; +export type { NoSsrProps } from './NoSsr.types'; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index abf036fee..b4a0e2c76 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -58,6 +58,7 @@ export * from './Menu'; export * from './MenuGroup'; export * from './MenuItem'; export * from './Modal'; +export * from './NoSsr'; export * from './OverlayScrollbars'; export * from './PageHGroup'; export * from './Pagination'; diff --git a/packages/react/src/overrides.ts b/packages/react/src/overrides.ts index c38a37754..7983554e5 100644 --- a/packages/react/src/overrides.ts +++ b/packages/react/src/overrides.ts @@ -106,6 +106,7 @@ import { MenuListProps, MenuProps } from './components/Menu'; import { MenuGroupProps } from './components/MenuGroup'; import { MenuItemProps } from './components/MenuItem'; import { ModalProps } from './components/Modal'; +import { NoSsrProps } from './components/NoSsr'; import { PageHGroupActionsProps, PageHGroupBreadcrumbsProps, @@ -289,6 +290,7 @@ declare module './theming/DefaultPropsProvider/DefaultPropsProvider.types' { ESMenuList: MenuListProps; ESMadeBy: MadeByProps; ESModal: ModalProps; + ESNoSsr: NoSsrProps; ESPageHGroup: PageHGroupProps; ESPageHGroupActions: PageHGroupActionsProps; ESPageHGroupBreadcrumbs: PageHGroupBreadcrumbsProps;