Next.js 中 use client 的误解及解法
1. 区分客户端组件和服务端组件
在 Next.js 中,默认情况下 src 下的组件都是服务端组件。如果需要在服务端组件中使用交互性功能(如 onClick 或 useState),会出现报错,因为这些功能只能在客户端组件中使用。
示例:导入按钮组件
在 page.tsx 中,我们导入了自定义的 Button 组件:
// app/page.tsx
import styles from './page.module.css'
import Button from './components/button'
export default function Home() {
return (
<main className={styles.main}>
<h1>Hello World</h1>
<Button />
</main>
)
}
如果 Button 组件需要客户端特性,可以为其单独加上 use client 标记,这样它就成为了客户端组件。切忌在 page.tsx 顶部直接添加 use client,因为这会使 page.tsx 的所有组件都变成客户端组件。
2. 避免不必要的客户端组件
如果在 page.tsx 中使用 use client,虽然不会报错,但会将所有子组件转换为客户端组件,导致性能开销增大。在以下示例中,如果 Post 组件需要加载较大的第三方库(如 sanitize-html),最好保持它为服务端组件。
'use client'
import styles from './page.module.css'
import Button from './components/button'
import Post from './components/post'
export default function Home() {
return (
<main className={styles.main}>
<h1>Hello World</h1>
<Button />
<Post />
</main>
)
}
3. 最优实践:按需使用客户端组件
仅对需要交互的最底层组件使用 use client,这样能确保更高效的性能。
4. 使用 Context 封装客户端组件
当需要使用上下文传递数据时,可以在客户端组件中使用 use client:
// app/context/ThemeContext.tsx
'use client'
import { useState, cloneElement, ReactNode, ReactElement } from 'react'
export default function ThemeContextProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'))
}
return <div>{cloneElement(children as ReactElement, { theme, toggleTheme })}</div>
}
在布局中封装时,不会改变子组件的服务端或客户端属性,因为**import 路径决定了组件属性,而不是渲染树**:
// app/layout.tsx
import ThemeContextProvider from './context/ThemeContext'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ThemeContextProvider>{children}</ThemeContextProvider>
</body>
</html>
)
}
5. 交互性组件的多次导入
如果将交互性组件(如 Button)分别导入到客户端和服务端组件中,它们会根据使用环境独立运行:
// Button Component
import style from './button.module.css'
export default function Button() {
return (
<div className={style.btn} onClick={() => console.log('click me')}>
Click me
</div>
)
}
// Post Component (Server Component)
import sanitizeHtml from 'sanitize-html'
import Button from './button'
export default function Post() {
return (
<div>
Post
<Button />
</div>
)
}
// Form Component (Client Component)
'use client'
import Button from './button'
export default function Form() {
return (
<div>
form <Button />
</div>
)
}
总结:
- 将交互性代码放在客户端组件中,以避免性能问题。
- 遵循导入树(import tree)来决定客户端或服务端属性,而不是关注渲染树(render tree)。