-
-
Save denk0403/be1365575f961b6349de89f7655ffedf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <SkipRenderOnClient shouldRenderOnClient={() => window.innerWidth <= 500}> | |
| <MyComponent /> | |
| </SkipRenderOnClient> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Activity, memo, useDeferredValue, useSyncExternalStore, type FC, type PropsWithChildren } from "react"; | |
| type SkipRenderOnClientProps = PropsWithChildren<{ | |
| /** | |
| * A function that returns a boolean indicating if the children should be | |
| * rendered on the client. | |
| * @returns `true` if the children should be rendered on the client, `false` otherwise. | |
| */ | |
| shouldRenderOnClient: () => boolean; | |
| }>; | |
| /** | |
| * Empty subscribe function. | |
| * @returns An empty function. | |
| */ | |
| const emptySubscribe = () => () => {}; | |
| const always = () => true; | |
| const never = () => false; | |
| /** | |
| * When using React's server-side rendering, we often need to render components | |
| * on the server even if they are conditional on the client e.g. hidden based on | |
| * window width. | |
| * | |
| * In order for hydration to succeed, the first client render must | |
| * match the DOM (which is generated from the HTML returned by the server), | |
| * otherwise we will get hydration mismatch errors. This means the component | |
| * must be rendered again during the first client render. | |
| * | |
| * However, hydration is expensive, so we really don't want to pay that penalty | |
| * only for the element to be hidden or removed immediately afterwards. | |
| * | |
| * For example, imagine we have two components: one for mobile and one for | |
| * desktop. Usually we would render both components on the server and on the | |
| * client (to avoid hydration mismatch errors) and toggle their visibility using | |
| * CSS. This means we would be hydrating both components even though only one of | |
| * them is currently shown to the user. | |
| * | |
| * `SkipRenderOnClient` conditionally skips hydrating children by using `Activity` to | |
| * defer their hydration to a low priority render while scheduling a high priority | |
| * render to remove them. This ensures there are no hydration mismatch errors. | |
| * | |
| * Following on from the example above, this is how we would apply | |
| * `SkipRenderOnClient`:. | |
| * | |
| * ```tsx | |
| * <SkipRenderOnClient shouldRenderOnClient={() => window.innerWidth <= 500}> | |
| * <MyMobileComponent className={styles.showOnMobile} /> | |
| * </SkipRenderOnClient> | |
| * <SkipRenderOnClient shouldRenderOnClient={() => window.innerWidth > 500}> | |
| * <MyDesktopComponent className={styles.showOnDesktop} /> | |
| * </SkipRenderOnClient> | |
| * ``` | |
| */ | |
| export const SkipRenderOnClient: FC<SkipRenderOnClientProps> = ({ | |
| children, | |
| shouldRenderOnClient, | |
| }) => { | |
| /** | |
| * Whether this component is rendering for the "server". | |
| * | |
| * During hydration, this hook will use the "server" value, | |
| * after which it will schedule a high priority render to update to the "client" value. | |
| * When mounting on the client, this hook will always use the "client" value. | |
| * @see {@link https://tkdodo.eu/blog/avoiding-hydration-mismatches-with-use-sync-external-store#usesyncexternalstore Avoiding Hydration Mismatches with useSyncExternalStore} | |
| * | |
| * `useDeferredValue` ensures the update however doesn't block the main thread, providing better INP. | |
| * @see {@link https://kurtextrem.de/posts/react-uses-hydration Concurrent Hydration with useSyncExternalStore} | |
| */ | |
| const isServer = useDeferredValue(useSyncExternalStore(emptySubscribe, never, always)); | |
| // For the "server", always render the children. | |
| if (isServer) { | |
| // Activity defers the hydration of the children to a low priority render. | |
| return <Activity>{children}</Activity>; | |
| } | |
| // For the "client", only render the children if the `shouldRenderOnClient` function returns `true`. | |
| if (shouldRenderOnClient()) { | |
| // Since the children's hydration is deferred to a low priority render, if the function returns | |
| // `false`, the children will be removed without ever being hydrated. | |
| return <Activity>{children}</Activity>; | |
| } | |
| return null; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment