Skip to content

Instantly share code, notes, and snippets.

@denk0403
Forked from OliverJAsh/SkipRenderOnClient.tsx
Last active March 18, 2026 07:26
Show Gist options
  • Select an option

  • Save denk0403/be1365575f961b6349de89f7655ffedf to your computer and use it in GitHub Desktop.

Select an option

Save denk0403/be1365575f961b6349de89f7655ffedf to your computer and use it in GitHub Desktop.
<SkipRenderOnClient shouldRenderOnClient={() => window.innerWidth <= 500}>
<MyComponent />
</SkipRenderOnClient>
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