Next.js 中 use client 的误解及解法

834 阅读2分钟

Next.js 中 use client 的误解及解法

1. 区分客户端组件和服务端组件

在 Next.js 中,默认情况下 src 下的组件都是服务端组件。如果需要在服务端组件中使用交互性功能(如 onClickuseState),会出现报错,因为这些功能只能在客户端组件中使用。

示例:导入按钮组件

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)。