原文地址:Communication Between Client Components in Next.js - This Dot Labs

NextJS 中,不同组件树下的客户端组件如何通信?
image.png

1. 将状态提升为客户端组件

通过 Context 实现类似于 Zustand 的状态管理方式,将状态提升到最外层的客户端组件内,为了不影响服务器组件的正常渲染,客户端组件需要使用 Context 包裹,并将 children 作为子组件渲染

"use client"
 
import { createContext, useState } from "react"
 
type WrapperContextValue = {
  counterValue: number
  increaseCounter: () => void
}
 
export const WrapperContext = createContext<WrapperContextValue>({
  counterValue: 0,
  increaseCounter: () => {},
})
 
export interface WrapperComponentProps {
  children?: React.ReactNode
}
 
export default function WrapperComponent({ children }: WrapperComponentProps) {
  const [counterValue, setCounterValue] = useState(0)
 
  return (
    <WrapperContext.Provider
      value={{
        counterValue,
        increaseCounter: () => {
          setCounterValue((prev) => prev + 1)
        },
      }}
    >
      {children}
    </WrapperContext.Provider>
  )
}

children 可以是服务器组件

export default function CommonClientComponentLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <div>
      <h1>Common Client Component Layout</h1>
      <WrapperComponent>{children}</WrapperComponent>
    </div>
  )
}

2. 通过 URL 查询参数处理

这种方式是将状态存储在 URL 上,通过 useSearchParams 来在不同的客户端组件内获取URL参数,修改状态时通过 router.replace 来修改参数,触发 useSearchParams Hook 的更新

状态更新组件

"use client"
import { useSearchParams, useRouter } from "next/navigation"
 
export default function Button() {
  const router = useRouter()
  const searchParams = useSearchParams()
  const currentValue = searchParams.get("counterValue") || "0"
 
  const handleClick = () => {
    const newValue = parseInt(currentValue) + 1
    const newSearchParams = new URLSearchParams(searchParams.toString())
    newSearchParams.set("counterValue", newValue.toString())
    router.replace(`?${newSearchParams.toString()}`)
  }
 
  return <button onClick={handleClick}>Increment</button>
}

状态获取组件

"use client"
import { useSearchParams } from "next/navigation"
 
export default function CounterDisplay() {
  const searchParams = useSearchParams()
  const currentValue = searchParams.get("counterValue") || "0"
 
  return <div>Counter Value: {currentValue}</div>
}

3. 将状态存储到服务器

计数器显示组件将计数器值作为属性接受,值是从服务器组件中从数据库等地方读取的。
按钮组件在点击时调用一个 Server Action ,更新计数器的值,并且调用 revalidatePath 或者 revalidateTag 来更新缓存,从而重新渲染计数器显示组件
Server Action

"use server"
 
import { revalidatePath } from "next/cache"
 
export async function incrementCounterAction() {
  // Call API/database to increment counter value
 
  // Revalidate the path to purge the caches and re-fetch the data
  revalidatePath("/storing-state-on-server")
}

按钮组件

"use client"
import { incrementCounterAction } from "@/app/storing-state-on-server/actions/actions"
 
export default function Button() {
  const handleClick = async () => {
    await incrementCounterAction()
  }
 
  return <button onClick={handleClick}>Increment</button>
}

计数器显示组件

"use client"
 
export type CounterDisplayProps = {
  counterValue: number
}
 
export default function CounterDisplay({ counterValue }: CounterDisplayProps) {
  return <div>Counter Value: {counterValue}</div>
}

父组件

import CounterDisplay from "@/app/storing-state-on-server/components/counter-display"
import Button from "@/app/storing-state-on-server/components/button"
 
async function getCounterValue() {
  // 从接口获取数据库中获取到值
  return Promise.resolve(0) // This would be an API/database call in a real app
}
 
export default async function StoringStateOnServerPage() {
  const counterValue = await getCounterValue()
  return (
    <div>
      <h1>Storing State on Server Page</h1>
      <CounterDisplay counterValue={counterValue} />
      <p>Some content goes here</p>
      <Button />
    </div>
  )
}