---
title: "How to Pass Data from Next.js Directly to Builder.io Components"
slug: "nextjs-builder-io-server-data-components"
published: "2025-09-03"
updated: "2025-09-03"
validated: "2025-10-20"
categories:
  - "Next.js"
tags:
  - "Builder.io"
  - "Next.js"
  - "server data"
  - "data prop"
  - "visual editor"
  - "headless CMS"
  - "data binding"
  - "server components"
  - "content state"
llm-intent: "reference"
audience-level: "intermediate"
framework-versions:
  - "next.js@15"
  - "builder.io@3"
  - "typescript@5"
  - "node@18+"
status: "stable"
llm-purpose: "Learn how to pass server data directly from Next.js to Builder.io components using the data prop. Maintain control over fetching, caching."
llm-prereqs:
  - "Access to Builder.io"
  - "Access to Next.js"
  - "Access to TypeScript"
  - "Access to Shopify API"
llm-outputs:
  - "Completed outcome: Learn how to pass server data directly from Next.js to Builder.io components using the data prop. Maintain control over fetching, caching."
---

**Summary Triples**
- (How to Pass Data from Next.js Directly to Builder.io Components, focuses-on, Learn how to pass server data directly from Next.js to Builder.io components using the data prop. Maintain control over fetching, caching.)
- (How to Pass Data from Next.js Directly to Builder.io Components, category, general)

### {GOAL}
Learn how to pass server data directly from Next.js to Builder.io components using the data prop. Maintain control over fetching, caching.

### {PREREQS}
- Access to Builder.io
- Access to Next.js
- Access to TypeScript
- Access to Shopify API

### {STEPS}
1. Follow the detailed walkthrough in the article content below.

<!-- llm:goal="Learn how to pass server data directly from Next.js to Builder.io components using the data prop. Maintain control over fetching, caching." -->
<!-- llm:prereq="Access to Builder.io" -->
<!-- llm:prereq="Access to Next.js" -->
<!-- llm:prereq="Access to TypeScript" -->
<!-- llm:prereq="Access to Shopify API" -->
<!-- llm:output="Completed outcome: Learn how to pass server data directly from Next.js to Builder.io components using the data prop. Maintain control over fetching, caching." -->

# How to Pass Data from Next.js Directly to Builder.io Components
> Learn how to pass server data directly from Next.js to Builder.io components using the data prop. Maintain control over fetching, caching.
Matija Žiberna · 2025-09-03

I was building a Next.js headless commerce site when I hit a frustrating limitation with Builder.io's data integrations. The built-in Shopify integration couldn't handle our complex metaobject structure, and the API integration felt like giving up control over our carefully crafted data layer. After diving deep into Builder.io's documentation and experimenting with different approaches, I discovered you can bypass all integrations entirely by passing data directly from your Next.js server components.

This guide shows you exactly how to maintain complete control over your data fetching, caching, and transformation while still giving content creators the visual editing experience they need.

## The Problem with Built-in Integrations

When you rely on Builder.io's integrations, you're essentially handing over control of your data layer. You become dependent on their API formats, caching strategies, and update frequencies. For complex applications where data relationships matter or where you have specific performance requirements, this creates unnecessary constraints.

What I needed was a way to fetch data server-side in Next.js using our existing infrastructure, then make that data available in Builder.io's visual editor. The solution lies in Builder.io's `data` prop - a powerful but underutilized feature that lets you inject any data directly into the visual editor's Content State.

## Understanding the Data Flow

The approach works by leveraging Next.js server components to fetch data, then passing it to Builder.io's `Content` component via the `data` prop. This makes your server data immediately available in Builder.io's visual editor under the "Content State" panel, where non-technical users can bind it to components without knowing anything about your backend implementation.

Here's how the flow works: your Next.js page fetches data server-side, transforms it into the shape you want, passes it to Builder.io's Content component, and that data becomes available in the visual editor as `state.yourData`.

## Setting Up Server-Side Data Fetching

Let's start with a practical example using Shopify metaobjects, though this pattern works with any data source. First, we'll create a server component that fetches both Builder.io content and our custom data in parallel.

```typescript
// File: app/events/page.tsx
import {
  Content,
  fetchOneEntry,
  getBuilderSearchParams,
  isPreviewing,
} from "@builder.io/sdk-react";
import { getMetaobjectsByType } from "lib/shopify";
import { Suspense } from "react";
import { Metadata } from "next";

interface PageProps {
  searchParams: Promise<Record<string, string>>;
}

const PUBLIC_API_KEY = process.env.NEXT_PUBLIC_BUILDER_IO_PUBLIC_KEY!;

export default async function EventsPage(props: PageProps) {
  const urlPath = "/events";
  const searchParams = await props.searchParams;

  try {
    // Fetch both Builder.io content and your data in parallel
    const [content, eventsMetaobjects] = await Promise.all([
      fetchOneEntry({
        options: getBuilderSearchParams(searchParams),
        apiKey: PUBLIC_API_KEY,
        model: "page",
        userAttributes: { urlPath },
      }),
      getMetaobjectsByType("events") // Your custom data fetching
    ]);

    const canShowContent = content || isPreviewing(searchParams);

    if (!canShowContent) {
      return (
        <div className="container mx-auto px-4 py-8 text-center">
          <h1 className="text-4xl font-bold mb-4">Events</h1>
          <p className="text-gray-600">
            This page is managed by Builder.io, but no content has been published yet.
          </p>
        </div>
      );
    }

    // Transform your data for Builder.io
    const events = eventsMetaobjects?.edges?.map(({ node }) => {
      const getFieldValue = (key: string) =>
        node.fields.find(field => field.key === key)?.value || '';

      return {
        id: node.id,
        handle: node.handle,
        name: getFieldValue('name'),
        startDate: getFieldValue('startdatum'),
        endDate: getFieldValue('enddatum'),
        address: getFieldValue('adresse'),
        description: getFieldValue('beschreibung'),
        image: getFieldValue('bild'),
        link: getFieldValue('eventlink'),
      };
    }) || [];

    // Prepare data for Builder.io
    const builderData = {
      events,
      eventsCount: events.length,
      hasEvents: events.length > 0,
      // Add helper data for content creators
      upcomingEvents: events.filter(event => {
        if (!event.startDate) return false;
        const startDate = new Date(event.startDate);
        return startDate >= new Date();
      }),
      pastEvents: events.filter(event => {
        if (!event.endDate && !event.startDate) return false;
        const endDate = new Date(event.endDate || event.startDate);
        return endDate < new Date();
      }),
    };

    return (
      <Content
        data={builderData}
        content={content}
        apiKey={PUBLIC_API_KEY}
        model="page"
      />
    );

  } catch (error) {
    console.error('Error loading events page:', error);

    return (
      <div className="container mx-auto px-4 py-8 text-center">
        <h1 className="text-4xl font-bold mb-4 text-red-600">Error Loading Events</h1>
        <p className="text-gray-600">
          Sorry, there was an error loading the events page.
        </p>
      </div>
    );
  }
}
```

This implementation fetches both Builder.io content and your custom data simultaneously using Promise.all, ensuring optimal performance. The key insight here is the `builderData` object - this becomes available in Builder.io's visual editor as `state.events`, `state.eventsCount`, and so on.

## Transforming Data for Visual Editors

The data transformation step is crucial because you're preparing data specifically for non-technical users who will be working in a visual interface. Notice how I've included helper properties like `upcomingEvents` and `pastEvents` - these make it easier for content creators to work with filtered data without needing to understand complex logic.

The transformation also flattens complex nested structures into simple key-value pairs that are intuitive to work with in Builder.io's binding interface. Instead of requiring content creators to navigate deep object hierarchies, they can simply access `item.name` or `item.startDate`.

## Making Data Available in the Visual Editor

Once you pass data through the `data` prop, it becomes immediately available in Builder.io's visual editor under the "Content State" panel in the Data tab. Content creators can now bind this data to any component without writing code or understanding your backend implementation.

### Basic Data Binding

For displaying individual pieces of data, they can use expressions like:
- `{{state.eventsCount}}` - Shows total number of events
- `{{state.hasEvents ? 'We have events!' : 'No events scheduled'}}` - Conditional content
- `{{state.events.length}}` - Dynamic count display

### Working with Lists

For lists of data, content creators can:

1. **Select any container element** (div, section, etc.)
2. **Go to the Data tab**
3. **Set "Repeat on" to `state.events`**
4. **Inside repeated elements**, access individual properties:
   - `{{item.name}}` - Event name
   - `{{item.startdatum}}` - Formatted start date
   - `{{item.adresse}}` - Event address
   - `{{item.beschreibung}}` - Event description

### Using Filtered Arrays

You can also use the pre-filtered arrays:
- **Repeat on `state.upcomingEvents`** for future events only
- **Repeat on `state.pastEvents`** for historical events
- This eliminates the need for content creators to write complex filtering logic

For comprehensive data binding documentation, see Builder.io's official guide: [Data Binding and State Management](https://www.builder.io/c/docs/data-binding-view-use-state)

This approach gives you the best of both worlds: developers maintain complete control over data fetching, caching, and transformation, while content creators get an intuitive visual interface for building pages with that data.

## Simplifying Data for Content Creators

One crucial step in preparing data for Builder.io is transforming complex technical formats into user-friendly, display-ready values. Content creators shouldn't need to parse ISO dates or extract text from complex JSON structures - they should get clean, ready-to-use data.

### The Challenge: Complex Data Structures

Raw data from APIs often comes in technical formats that are difficult for non-technical users to work with:

```typescript
// Before: Complex, technical data
{
  startdatum: "2025-09-10T07:00:00Z",        // ISO date format
  enddatum: "2025-09-14T16:00:00Z",          // ISO date format
  beschreibung: `{                           // Nested JSON structure
    "type": "root",
    "children": [{
      "type": "paragraph", 
      "children": [{
        "type": "text",
        "value": "Halle 2, Stand: M 12"
      }]
    }]
  }`
}
```

### The Solution: Data Transformation Utilities

Create utility functions that transform complex data into simple, display-ready formats:

```typescript
// File: lib/utils.ts
export const formatDateToGerman = (isoDateString: string): string => {
  if (!isoDateString) return '';

  try {
    const date = new Date(isoDateString);
    if (isNaN(date.getTime())) return '';

    const options: Intl.DateTimeFormatOptions = {
      day: 'numeric',
      month: 'long', 
      year: 'numeric',
      hour: '2-digit',
      minute: '2-digit',
      timeZone: 'Europe/Berlin'
    };

    return date.toLocaleString('de-DE', options);
  } catch (error) {
    console.warn('Error formatting date:', error);
    return '';
  }
};

export const extractPlainTextFromRichText = (jsonString: string): string => {
  if (!jsonString || typeof jsonString !== 'string') return '';

  try {
    const parsed = JSON.parse(jsonString);

    const extractText = (node: any): string => {
      if (!node || typeof node !== 'object') return '';

      if (node.type === 'text' && node.value) {
        return node.value;
      }

      if (node.children && Array.isArray(node.children)) {
        return node.children
          .map((child: any) => extractText(child))
          .filter(Boolean)
          .join(' ');
      }

      return '';
    };

    return extractText(parsed).trim();
  } catch (error) {
    console.warn('Error parsing rich text JSON:', error);
    return jsonString; // Fallback to original string
  }
};
```

### Applying Transformations

Use these utilities in your data transformation pipeline:

```typescript
// Transform metaobjects for Builder.io with simplified data structure
const events = eventsMetaobjects?.edges?.map(({ node }) => {
  const getFieldValue = (key: string) =>
    node.fields.find(field => field.key === key)?.value || '';

  const rawStartDatum = getFieldValue('startdatum');
  const rawEndDatum = getFieldValue('enddatum');
  const rawBeschreibung = getFieldValue('beschreibung');

  return {
    id: node.id,
    handle: node.handle,
    name: getFieldValue('name'),
    startdatum: formatDateToGerman(rawStartDatum),         // ✨ Simplified
    enddatum: formatDateToGerman(rawEndDatum),             // ✨ Simplified  
    adresse: getFieldValue('adresse'),
    beschreibung: extractPlainTextFromRichText(rawBeschreibung), // ✨ Simplified
    bild: getFieldValue('bild'),
    eventlink: getFieldValue('eventlink'),
  };
}) || [];
```

### The Result: Clean, Usable Data

Now content creators work with simplified, display-ready data:

```typescript
// After: Clean, user-friendly data
{
  startdatum: "10. September 2025, 07:00",   // Human-readable German format
  enddatum: "14. September 2025, 16:00",     // Human-readable German format
  beschreibung: "Halle 2, Stand: M 12"       // Plain text, ready to display
}
```

This transformation eliminates confusion and makes data binding straightforward. Instead of complex expressions, content creators can simply use `{{item.startdatum}}` and get perfectly formatted output.

## Adding Error Handling and Loading States

Production applications need robust error handling and loading states. The implementation above includes both error boundaries and loading states to ensure a smooth user experience even when things go wrong.

```typescript
// File: app/events/page.tsx (additional loading component)
function EventsPageSkeleton() {
  return (
    <div className="container mx-auto px-4 py-8">
      <div className="animate-pulse">
        <div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
        <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
          {[...Array(6)].map((_, i) => (
            <div key={i} className="bg-gray-100 rounded-lg p-6">
              <div className="h-4 bg-gray-200 rounded mb-2"></div>
              <div className="h-4 bg-gray-200 rounded w-2/3 mb-4"></div>
              <div className="h-32 bg-gray-200 rounded"></div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// Then wrap your Content component
<Suspense fallback={<EventsPageSkeleton />}>
  <Content
    data={builderData}
    content={content}
    apiKey={PUBLIC_API_KEY}
    model="page"
  />
</Suspense>
```

The error handling ensures that if your data fetching fails, users still see a meaningful message rather than a broken page. The loading states provide immediate feedback while server-side rendering completes, creating a professional user experience.

## Extending to Any Data Source

This pattern works with any data source, not just Shopify. Whether you're fetching from a headless CMS, a REST API, a database, or even multiple sources, the approach remains the same: fetch server-side, transform for your needs, and pass through the `data` prop.

```typescript
// File: app/blog/page.tsx (example with different data source)
const [content, blogPosts, authors] = await Promise.all([
  fetchOneEntry({...}),
  fetchBlogPosts(),
  fetchAuthors()
]);

const builderData = {
  posts: blogPosts.map(post => ({
    title: post.title,
    excerpt: post.excerpt,
    author: authors.find(a => a.id === post.authorId),
    publishedAt: post.publishedAt,
    category: post.category
  })),
  categories: [...new Set(blogPosts.map(p => p.category))],
  featuredPosts: blogPosts.filter(p => p.featured)
};
```

The key is thinking about how content creators will want to use the data and structuring it accordingly. Include computed properties, filtered arrays, and helper values that make their job easier.

By taking this approach, you've eliminated dependency on Builder.io's integrations while giving content creators even more power than they'd have with built-in integrations. You control the data pipeline completely - from fetching and caching strategies to the exact shape of data that reaches the visual editor.

This method has transformed how I build headless applications with Builder.io. Instead of fighting against integration limitations, I now design my data layer exactly how I want it, then make it available to content creators through a clean, intuitive interface. The result is faster, more maintainable applications where both developers and content creators can work effectively within their areas of expertise.

Let me know in the comments if you have questions about implementing this pattern with your specific data sources, and subscribe for more practical development guides.

Thanks, Matija

## LLM Response Snippet
```json
{
  "goal": "Learn how to pass server data directly from Next.js to Builder.io components using the data prop. Maintain control over fetching, caching.",
  "responses": [
    {
      "question": "What does the article \"How to Pass Data from Next.js Directly to Builder.io Components\" cover?",
      "answer": "Learn how to pass server data directly from Next.js to Builder.io components using the data prop. Maintain control over fetching, caching."
    }
  ]
}
```