Rendering multiple custom component client side and server side

i’m using nextjs ( app router ) with builder, created some custom components, which fetch data from posts

categgoryposts.jsx component

  "use client";

import React, { useEffect, useState } from "react";
import { Row, Col, Typography, Space } from "antd";
import { useIsPreviewing } from "@builder.io/react";
import Link from "next/link";
import builderApi from "@/utils/builderApi";
import PostCard from "./PostCard";
import "./styles.scss";

const { Title } = Typography;

export default function CategoryPosts({
  category,
  posts: initialPosts = [],
  markets = [],
}) {
  const isPreviewing = useIsPreviewing();
  const [displayPosts, setDisplayPosts] = useState(initialPosts);
  const [loading, setLoading] = useState(false);

  // Get the current category title
  const currentCategoryTitle =
    category?.value?.data?.title || category?.data?.title;

  // Fetch posts whenever the category title changes in preview mode
  useEffect(() => {
    if (isPreviewing) {
      const fetchPreviewPosts = async () => {
        try {
          setLoading(true);
          console.log("Fetching posts for category:", category);
          const previewPosts = await builderApi.getPostsByCategory(category);
          if (previewPosts) {
            setDisplayPosts(previewPosts);
          }
        } catch (error) {
          console.error("Error fetching preview posts:", error);
          setDisplayPosts([]);
        } finally {
          setLoading(false);
        }
      };

      fetchPreviewPosts();
    }
  }, [isPreviewing, category]);

  return (
    <div style={{ padding: "0 16px" }}>
      <Row gutter={24}>
        <Col xs={24} lg={18}>
          <Title
            level={5}
            style={{ margin: "0 0 16px  0" }}
            className="heading-style"
          >
            {currentCategoryTitle || "Category Posts"}
          </Title>
          {loading ? (
            <div>Loading posts...</div>
          ) : displayPosts && displayPosts.length > 0 ? (
            <Row gutter={[16, 32]}>
              {displayPosts.map((post, index) => (
                <Col
                  xs={24}
                  sm={12}
                  lg={8}
                  key={index}
                  style={{ marginBottom: 16 }}
                >
                  <Link href={`${post.data.url}`}>
                    <PostCard post={post} />
                  </Link>
                </Col>
              ))}
            </Row>
          ) : (
            <div>No posts found for this category</div>
          )}
        </Col>
      </Row>
    </div>
  );
}

other posts comonent

"use client";
import React, { useEffect, useState } from "react";
import { Row, Col, Typography } from "antd";
import { useIsPreviewing } from "@builder.io/react";
import Link from "next/link";
import builderApi from "@/utils/builderApi";
import PostCard from "./PostCard";
import "./styles.scss";

const { Title } = Typography;

export default function PostAll({ latestPosts = [] }) {
  const isPreviewing = useIsPreviewing();
  const [displayPosts, setDisplayPosts] = useState(latestPosts);
  const [loading, setLoading] = useState(false);
  useEffect(() => {
    if (isPreviewing) {
      const fetchPreviewPosts = async () => {
        try {
          setLoading(true);
          const fetched = await builderApi.getBlogPosts();
          if (fetched) {
            setDisplayPosts(fetched);
          }
        } catch (error) {
          console.error("Error fetching preview posts:", error);
          setDisplayPosts([]);
        } finally {
          setLoading(false);
        }
      };

      fetchPreviewPosts();
    }
  }, []);

  return (
    <div style={{ padding: "0 16px", width: "100%" }}>
      <Title level={5} style={{ marginBottom: "24px" }}>
        Latest Posts
      </Title>

      {loading ? (
        <div>Loading posts</div>
      ) : displayPosts.length > 0 ? (
        <Row gutter={[16, 32]}>
          {displayPosts.map((post) => (
            <Col
              key={post.id || post.data.url}
              xs={24}
              sm={12}
              md={8}
              lg={6}
              style={{ marginBottom: 16 }}
            >
              <Link href={post.data.url}>
                <PostCard post={post} />
              </Link>
            </Col>
          ))}
        </Row>
      ) : (
        <div>No posts found</div>
      )}
    </div>
  );
}

both are registere as custsom comnponent and on dynamic page route i’m using getasyncprop

 async "Category Posts"({ category, postsLimit = 6 }) {
      const referencedCategoryId = category?.id;

      if (!referencedCategoryId) {
        return { posts: [] };
      }
      try {
        const posts = await builderApi.getPostsByCategory(category);
        if (!posts || posts.length === 0) {
          return { posts: [] };
        }
        const sortedPosts = posts.sort(
          (a, b) =>
            new Date(b.data?.date || b.data?.publishDate).getTime() -
            new Date(a.data?.date || a.data?.publishDate).getTime()
        );
        return { posts: sortedPosts.slice(0, postsLimit) };
      } catch (error) {
        return { posts: [] };
      }
    },

in builder preview i’m calling

categoryPost
Posts

when i first land on dynamic page route, categorypost works just fine, but posts doesnt’ fetch anything because isPreviewing might not be available or false

can someone guide me to right direction, and another thing if i. change option for post comnponent categorypost get re-renderd because of isPreviewing ?

Hi @joji ,

  1. PostAll not loading in preview mode initially:
    This happens because useIsPreviewing() might return false on the first render—even inside the Builder preview. That’s expected since it’s async and client-only. You can ensure that PostAll gets data even before isPreviewing resolves.
    Fetch posts if displayPosts is empty:
    Update your useEffect like this:
useEffect(() => {
  const fetchPreviewPosts = async () => {
    try {
      setLoading(true);
      const fetched = await builderApi.getBlogPosts();
      setDisplayPosts(fetched || []);
    } catch (error) {
      console.error("Error fetching preview posts:", error);
      setDisplayPosts([]);
    } finally {
      setLoading(false);
    }
  };

  if (isPreviewing || displayPosts.length === 0) {
    fetchPreviewPosts();
  }
}, [isPreviewing]);

This ensures preview content loads regardless of isPreviewing being late and prevents empty states during Builder page load.

  1. Changing inputs in one component re-renders the other

Yes, changing inputs in Builder preview can cause re-renders of other components using useIsPreviewing. To reduce unnecessary network requests, you can debounce or cache API calls. This prevents excessive refetches when isPreviewing changes slightly or rerenders happen across components.

const [hasFetched, setHasFetched] = useState(false);

useEffect(() => {
  if ((isPreviewing && !hasFetched) || displayPosts.length === 0) {
    const fetchPreviewPosts = async () => {
      try {
        setLoading(true);
        const posts = await builderApi.getPostsByCategory(category);
        setDisplayPosts(posts || []);
        setHasFetched(true);
      } catch (e) {
        console.error(e);
        setDisplayPosts([]);
      } finally {
        setLoading(false);
      }
    };

    fetchPreviewPosts();
  }
}, [category, isPreviewing]);

Thanks,

Thanks, will give it a try