Skip to content

Data Flow & Server vs Client Components

Understanding how data flows through BlocksWeb is essential. This guide covers the complete flow from editor to component and the critical server/client architecture.

The Magic: Automatic Props

What Makes BlocksWeb Different

Zero state management boilerplate. Define editable fields in your schema, and BlocksWeb automatically passes editor values as props to your component. No useState, no useEffect, no prop drilling, no context providers - your component just receives the data it needs.

typescript
// You define the schema
Hero.schema = {
  displayName: 'Hero',
  options: [
    { type: 'text', name: 'title', label: 'Title', default: 'Welcome' },
    { type: 'text', name: 'subtitle', label: 'Subtitle', default: 'Get started' }
  ]
};

// User edits in the visual editor
// → title becomes "My Amazing Site"
// → subtitle becomes "Let's build something great"

// BlocksWeb automatically passes these as props!
const Hero = ({ title, subtitle }) => {
  // title = "My Amazing Site"
  // subtitle = "Let's build something great"
  return (
    <section>
      <h1>{title}</h1>      {/* Renders: My Amazing Site */}
      <p>{subtitle}</p>     {/* Renders: Let's build something great */}
    </section>
  );
};

No useState, no useEffect, no prop drilling - just props!

Complete Data Flow

┌─────────────────────────────────────────────────────────┐
│ 1. EDITOR: User changes "title" to "New Title"         │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 2. UPDATE: updateBlockProps({ title: "New Title" })    │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 3. AUTO-SAVE: Save to API after 3 seconds              │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 4. RE-RENDER: Component receives new props             │
│    const Hero = ({ title }) => <h1>{title}</h1>        │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 5. DISPLAY: "New Title" shown in editor and live site  │
└─────────────────────────────────────────────────────────┘

Critical: Server Components

CRITICAL - READ THIS FIRST

BlocksWeb components are SERVER COMPONENTS by default. This means:

  1. ✅ Props are automatically passed from the editor
  2. ❌ They CANNOT use React hooks (useState, useEffect, etc.)
  3. ❌ They CANNOT have event handlers (onClick, onChange, etc.)
  4. NEVER add "use client" to a BlocksWeb component - it will break the editor!

If you need interactivity or hooks, create a separate .client.tsx file.

Server Components (Default)

BlocksWeb components render on the server. They receive props and return JSX.

typescript
import { IBlockswebComponent } from '@blocksweb/core';

// This is a SERVER component
const Hero: IBlockswebComponent = ({ title, subtitle, buttonText }) => {
  // ✅ Props automatically passed from editor
  // ✅ Runs on server
  // ❌ No hooks allowed
  // ❌ No event handlers

  return (
    <section>
      <h1>{title}</h1>
      <p>{subtitle}</p>
      {/* This button won't respond to clicks - server component! */}
      <button>{buttonText}</button>
    </section>
  );
};

Hero.schema = {
  displayName: 'Hero Section',
  options: [
    { type: 'text', name: 'title', label: 'Title', default: 'Welcome' },
    { type: 'text', name: 'subtitle', label: 'Subtitle', default: 'Get started' },
    { type: 'text', name: 'buttonText', label: 'Button Text', default: 'Click me' }
  ]
};

export default Hero;

What You CAN Do

Receive props from editor

typescript
const Hero = ({ title, subtitle, image }) => {
  // All values come from the visual editor!
  return <section>...</section>;
};

Use utility components

typescript
import { RichText, Image, BlockOutlet } from '@blocksweb/core/client';

return (
  <div>
    <RichText propName="content" text={content} />
    <Image propName="hero" asset={heroImage} />
    <BlockOutlet propName="sections" blocks={sections} />
  </div>
);

Conditional rendering

typescript
return (
  <div>
    {showButton && <button>{buttonText}</button>}
    {image && <img src={image} alt={title} />}
  </div>
);

Map over data

typescript
return (
  <div>
    {items.map((item, i) => (
      <div key={i}>{item.title}</div>
    ))}
  </div>
);

What You CANNOT Do

React Hooks

typescript
// ❌ ERROR - Server components can't use hooks
const Hero = ({ title }) => {
  const [count, setCount] = useState(0);  // Error!
  useEffect(() => { ... });                // Error!
  const ref = useRef(null);                // Error!

  return <div>...</div>;
};

Event Handlers

typescript
// ❌ ERROR - Won't work on server
const Hero = ({ title }) => {
  return (
    <button onClick={() => alert('Hi')}>  // Won't work!
      Click Me
    </button>
  );
};

Browser APIs

typescript
// ❌ ERROR - No window/document on server
const Hero = () => {
  const width = window.innerWidth;        // Error: window is undefined
  localStorage.setItem('key', 'value');   // Error: localStorage is undefined
  document.querySelector('.foo');         // Error: document is undefined

  return <div>...</div>;
};

"use client" Directive

typescript
// ❌ CRITICAL ERROR - BREAKS THE EDITOR!
'use client';  // DON'T DO THIS!

const Hero: IBlockswebComponent = ({ title }) => {
  return <div>{title}</div>;  // Editor will break!
};

Client Components (.client.tsx)

When you need interactivity, create a separate client component:

File Structure

components/
├── ContactForm.tsx        # BlocksWeb component (server) - receives props
└── ContactForm.client.tsx # Client component (browser) - handles interactivity

Server Component: Receives Props

typescript
// components/ContactForm.tsx
import { IBlockswebComponent } from '@blocksweb/core';
import { ContactFormClient } from './ContactForm.client';

type ContactFormProps = {
  title: string;
  submitButtonText: string;
  successMessage: string;
};

// ✅ Server component - receives props from editor
const ContactForm: IBlockswebComponent<ContactFormProps> = ({
  title,
  submitButtonText,
  successMessage
}) => {
  // Just pass props through to client component
  return (
    <ContactFormClient
      title={title}
      submitButtonText={submitButtonText}
      successMessage={successMessage}
    />
  );
};

// Editor controls these props!
ContactForm.schema = {
  displayName: 'Contact Form',
  options: [
    { type: 'text', name: 'title', label: 'Form Title', default: 'Contact Us' },
    { type: 'text', name: 'submitButtonText', label: 'Submit Button', default: 'Send' },
    { type: 'text', name: 'successMessage', label: 'Success Message', default: 'Thanks!' }
  ]
};

export default ContactForm;

Client Component: Handles Interactivity

typescript
// components/ContactForm.client.tsx
'use client';  // ✅ This is OK here!

import { useState } from 'react';

type ContactFormClientProps = {
  title: string;
  submitButtonText: string;
  successMessage: string;
};

export function ContactFormClient({
  title,              // From editor via server component
  submitButtonText,   // From editor via server component
  successMessage      // From editor via server component
}: ContactFormClientProps) {
  // ✅ Now hooks work!
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [status, setStatus] = useState<'idle' | 'success'>('idle');

  // ✅ Event handlers work!
  const handleSubmit = async (e) => {
    e.preventDefault();
    // Submit logic...
    setStatus('success');
  };

  if (status === 'success') {
    return <div>{successMessage}</div>;  {/* Editable in CMS! */}
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>{title}</h2>  {/* Editable in CMS! */}
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit">
        {submitButtonText}  {/* Editable in CMS! */}
      </button>
    </form>
  );
}

The Power of Automatic Props

Example: Blog Post Component

typescript
// components/BlogPost.tsx
import { IBlockswebComponent } from '@blocksweb/core';
import { RichText, Image } from '@blocksweb/core/client';

type BlogPostProps = {
  title: string;
  author: string;
  date: string;
  featuredImage: string;
  content: string;
  showAuthor: boolean;
};

const BlogPost: IBlockswebComponent<BlogPostProps> = ({
  title,           // ✅ Automatically from editor
  author,          // ✅ Automatically from editor
  date,            // ✅ Automatically from editor
  featuredImage,   // ✅ Automatically from editor
  content,         // ✅ Automatically from editor
  showAuthor       // ✅ Automatically from editor
}) => {
  return (
    <article>
      {/* Featured image - editable via asset manager */}
      <Image
        propName="featuredImage"
        asset={featuredImage}
        alt={title}
        className="w-full h-[400px] object-cover"
      />

      {/* Title - editable text field */}
      <h1>{title}</h1>

      {/* Author info - conditional on checkbox */}
      {showAuthor && (
        <div className="author">
          <span>{author}</span>
          <span>{date}</span>
        </div>
      )}

      {/* Rich content - editable with formatting */}
      <RichText
        propName="content"
        text={content}
        className="prose"
      />
    </article>
  );
};

// Define what's editable
BlogPost.schema = {
  displayName: 'Blog Post',
  options: [
    {
      type: 'text',
      name: 'title',
      label: 'Post Title',
      default: 'My Blog Post'
    },
    {
      type: 'image',
      name: 'featuredImage',
      label: 'Featured Image',
      default: '/placeholder.jpg'
    },
    {
      type: 'text',
      name: 'author',
      label: 'Author Name',
      default: 'John Doe'
    },
    {
      type: 'text',
      name: 'date',
      label: 'Publish Date',
      default: '2024-01-01'
    },
    {
      type: 'richtext',
      name: 'content',
      label: 'Post Content',
      default: '<p>Write your post here...</p>'
    },
    {
      type: 'checkbox',
      name: 'showAuthor',
      label: 'Show Author Info',
      default: true
    }
  ]
};

export default BlogPost;

User experience:

  1. User adds "Blog Post" component to page
  2. All fields show defaults
  3. User edits title → component receives new title prop → re-renders
  4. User uploads image → component receives new featuredImage prop → re-renders
  5. User toggles "Show Author" → component receives new showAuthor prop → re-renders

No state management code needed!

When to Use Each Pattern

Server Component Only

Use when component is purely presentational:

typescript
// ✅ Server-only: No interactivity needed
const Hero = ({ title, subtitle, image }) => (
  <section>
    <h1>{title}</h1>
    <p>{subtitle}</p>
    <img src={image} alt={title} />
  </section>
);

Examples:

  • Hero sections
  • Feature grids
  • Testimonials
  • Blog posts
  • Product displays
  • Headers/footers

Server + Client Pattern

Use when you need interactivity:

typescript
// ✅ Server + Client: Form needs interactivity
// Server component receives props from editor
const Newsletter = (props) => (
  <NewsletterClient {...props} />
);

// Client component handles form state
'use client';
export function NewsletterClient({ title, buttonText }) {
  const [email, setEmail] = useState('');
  // ... form logic
}

Examples:

  • Forms
  • Carousels
  • Modals
  • Accordions
  • Search bars
  • Shopping carts
  • Interactive galleries

Why "use client" Breaks BlocksWeb Components

When you add "use client" to a BlocksWeb component:

typescript
'use client';  // ❌ BREAKS EVERYTHING!

const Hero: IBlockswebComponent = ({ title }) => {
  return <h1>{title}</h1>;
};

What breaks:

  1. ❌ Editor cannot render component in iframe
  2. ❌ Component selection doesn't work
  3. ❌ Props updates don't reflect
  4. ❌ Auto-save fails
  5. ❌ Visual editing completely broken

Why it breaks:

  • BlocksWeb editor expects server components for rendering
  • Editor needs to inspect and modify component structure
  • Client components run in browser, editor runs in different context

Solution:

typescript
// ✅ CORRECT: Server component
const Hero: IBlockswebComponent = ({ title }) => {
  return <HeroClient title={title} />;
};

// ✅ CORRECT: Separate client component
'use client';
export function HeroClient({ title }) {
  const [isVisible, setIsVisible] = useState(true);
  return isVisible ? <h1>{title}</h1> : null;
}

Summary

✅ Do This

  • Define schema with editable options
  • Let BlocksWeb automatically pass props
  • Keep BlocksWeb components as server components
  • Create .client.tsx files for interactivity
  • Pass editor-controlled props through to client components

❌ Don't Do This

  • Don't use hooks in BlocksWeb components
  • Don't add "use client" to BlocksWeb components
  • Don't try to manage editor state yourself
  • Don't use event handlers in server components
  • Don't access browser APIs in server components

Next Steps