You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Server Components是否适合基于客户端状态获取局部页面?

解决方案:Server Actions + 客户端组件组合

你的场景完全适合用Server Actions + 客户端组件来实现,既避免整页重渲染,又能安全地从数据库动态获取带参数的内容。

核心思路

  • 地图和弹窗必须是客户端组件:因为需要维护交互状态(比如选中的标记、弹窗显示状态),这类组件能在客户端本地更新,不会触发整页重渲染。
  • Server Actions负责带参数的数据库查询:在客户端组件里直接调用Server Action,传递选中标记的slug作为参数,服务器端执行查询后返回结果,全程不会刷新页面。

具体实现步骤

1. 定义Server Action(服务器端执行数据库查询)

在app/actions.ts中创建用于查询标记详情的Server Action:

'use server';

import { db } from '@/lib/db'; // 替换成你的数据库连接

export async function getMarkerDetails(slug: string) {
  const marker = await db.marker.findUnique({
    where: { slug },
    select: { title: true, description: true, extraData: true } // 按需选择字段
  });

  if (!marker) {
    throw new Error('标记不存在');
  }

  return marker;
}

2. 编写客户端地图组件(带弹窗交互)

创建app/components/MapWithPopup.tsx客户端组件,处理地图标记点击、弹窗状态更新:

'use client';

import { useState } from 'react';
import Map, { Marker, Popup } from 'react-map-gl';
import { getMarkerDetails } from '@/app/actions';

// 标记基础数据类型
type MarkerBase = {
  id: string;
  slug: string;
  lat: number;
  lng: number;
};

export default function MapWithPopup({ markers }: { markers: MarkerBase[] }) {
  const [selectedMarker, setSelectedMarker] = useState<MarkerBase | null>(null);
  const [popupData, setPopupData] = useState<{ title: string; description: string } | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  const handleMarkerClick = async (marker: MarkerBase) => {
    setIsLoading(true);
    setSelectedMarker(marker);

    try {
      // 调用Server Action,传递选中标记的slug
      const details = await getMarkerDetails(marker.slug);
      setPopupData({
        title: details.title,
        description: details.description
      });
    } catch (err) {
      console.error('加载标记详情失败:', err);
      setPopupData(null);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <Map
      initialViewState={{ latitude: 30.2672, longitude: -97.7431, zoom: 12 }} // 替换成你的初始地图坐标
      style={{ width: '100%', height: '80vh' }}
      mapStyle="mapbox://styles/mapbox/streets-v12"
    >
      {/* 渲染所有标记 */}
      {markers.map((marker) => (
        <Marker
          key={marker.id}
          latitude={marker.lat}
          longitude={marker.lng}
          onClick={() => handleMarkerClick(marker)}
        />
      ))}

      {/* 动态弹窗 */}
      {selectedMarker && (
        <Popup
          latitude={selectedMarker.lat}
          longitude={selectedMarker.lng}
          onClose={() => {
            setSelectedMarker(null);
            setPopupData(null);
          }}
        >
          {isLoading ? (
            <div>加载中...</div>
          ) : popupData ? (
            <>
              <h3>{popupData.title}</h3>
              <p>{popupData.description}</p>
            </>
          ) : (
            <div>无法加载详情</div>
          )}
        </Popup>
      )}
    </Map>
  );
}

3. 在页面组件中整合(用RSC渲染基础标记数据)

在app/page.tsx(React Server Component)中,先获取所有标记的基础数据(经纬度、slug等),再传给客户端地图组件:

import { db } from '@/lib/db';
import MapWithPopup from '@/app/components/MapWithPopup';

export default async function MapPage() {
  // 服务器端获取所有标记的基础信息(避免客户端暴露敏感数据)
  const markers = await db.marker.findMany({
    select: { id: true, slug: true, lat: true, lng: true }
  });

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">交互式地图</h1>
      <MapWithPopup markers={markers} />
    </div>
  );
}

为什么这个方案适合你

  • 仅更新弹窗内容:地图始终保持渲染状态,点击标记只会触发弹窗数据的局部更新,不会重绘整个地图。
  • 安全查询数据库:Server Action在服务器端执行,不会将数据库连接信息暴露给客户端。
  • 兼顾性能:页面初始的标记列表用RSC渲染,保证首屏加载速度;弹窗数据按需查询,减少不必要的请求。

内容的提问来源于stack exchange,提问作者Mat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 04:46:30