Client Only
在 React Router 初期支持 ssr 模型,要使用 React Router(Remix) 就必须熟悉 SSR。此时 ClientOnly 组件就产生了。
ClientOnly 就是在客户端选择的组件。
问题
- 图标
- @ant-design/pro-compoennts 等等组件都是客户端才需要渲染的,与我们的服务端 ssr 没有关系,也没有必要。
我们可以使用 ClientOnly 做服务端最小化 ssr 渲染, 渲染放到客户端渲染。但是就此也带来新的挑战。
你需要了解 React/React Router 的渲染机制。
渲染机制
- 子组件优先父组件执行, useEffect 也是子组件优先执行,这将都数据流造成影响
安装 remix-utils
sh
pnpm add remix-utils
使用
ts
function Compo() {
return <ClientOnly fallback={<FakeChart />}>{() => <Chart />}</ClientOnly>;
}
ClientOnly 的实现
tsx
import * as React from "react";
import { useHydrated } from "./use-hydrated.js";
type Props = {
/**
* You are encouraged to add a fallback that is the same dimensions
* as the client rendered children. This will avoid content layout
* shift which is disgusting
*/
children(): React.ReactNode;
fallback?: React.ReactNode;
};
/**
* Render the children only after the JS has loaded client-side. Use an optional
* fallback component if the JS is not yet loaded.
*
* Example: Render a Chart component if JS loads, renders a simple FakeChart
* component server-side or if there is no JS. The FakeChart can have only the
* UI without the behavior or be a loading spinner or skeleton.
* ```tsx
* return (
* <ClientOnly fallback={<FakeChart />}>
* {() => <Chart />}
* </ClientOnly>
* );
* ```
*/
export function ClientOnly({ children, fallback = null }: Props) {
return useHydrated() ? <>{children()}</> : <>{fallback}</>;
}
useHyDrated 的实现
ts
import { useSyncExternalStore } from "react";
function subscribe() {
// biome-ignore lint/suspicious/noEmptyBlockStatements: Mock function
return () => {};
}
/**
* Return a boolean indicating if the JS has been hydrated already.
* When doing Server-Side Rendering, the result will always be false.
* When doing Client-Side Rendering, the result will always be false on the
* first render and true from then on. Even if a new component renders it will
* always start with true.
*
* Example: Disable a button that needs JS to work.
* ```tsx
* let hydrated = useHydrated();
* return (
* <button type="button" disabled={!hydrated} onClick={doSomethingCustom}>
* Click me
* </button>
* );
* ```
*/
export function useHydrated() {
return useSyncExternalStore(
subscribe,
() => true,
() => false,
);
}
问题: ClientOnly 对 useEffect 造成两次渲染
以下是一个示例:
tsx
import {
type RouteConfig,
index,
route,
layout,
} from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
layout("./routes/layout.tsx", [route("about", "routes/about.tsx")]),
] satisfies RouteConfig;
about 页面是来有 layout,为了防止子组件在 layout 之前渲染,我们需要 data 存在我们想要的数据之前拦截。
tsx
import { useEffect, useState } from "react";
import { Outlet } from "react-router";
export default function Layout() {
const [data, setData] = useState(null)
useEffect(() => {
setData([1])
console.log("layout")
}, [])
if(!data) {
return <div>loading...</div>
}
return <div>
xxxx
<Outlet />
</div>
}
目标
- 不能让 ClientOnly 造成 useEffect 执行两次
- 不能让 子组件先渲染,先获取数据,保持父组件先过去数据
不让 ClientOnly 造成 useEffect 执行两次
不做 Root 级别的 ClientOnly 只在需要使用的地方执行一次。
不能让 子组件先渲染,先获取数据,保持父组件先过去数据
在获取到实现之前不渲染子组件