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.
// 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:
- ✅ Props are automatically passed from the editor
- ❌ They CANNOT use React hooks (
useState,useEffect, etc.) - ❌ They CANNOT have event handlers (
onClick,onChange, etc.) - ❌ 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.
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
const Hero = ({ title, subtitle, image }) => {
// All values come from the visual editor!
return <section>...</section>;
};✅ Use utility components
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
return (
<div>
{showButton && <button>{buttonText}</button>}
{image && <img src={image} alt={title} />}
</div>
);✅ Map over data
return (
<div>
{items.map((item, i) => (
<div key={i}>{item.title}</div>
))}
</div>
);What You CANNOT Do
❌ React Hooks
// ❌ 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
// ❌ ERROR - Won't work on server
const Hero = ({ title }) => {
return (
<button onClick={() => alert('Hi')}> // Won't work!
Click Me
</button>
);
};❌ Browser APIs
// ❌ 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
// ❌ 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 interactivityServer Component: Receives Props
// 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
// 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
// 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:
- User adds "Blog Post" component to page
- All fields show defaults
- User edits title → component receives new
titleprop → re-renders - User uploads image → component receives new
featuredImageprop → re-renders - User toggles "Show Author" → component receives new
showAuthorprop → re-renders
No state management code needed!
When to Use Each Pattern
Server Component Only
Use when component is purely presentational:
// ✅ 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:
// ✅ 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:
'use client'; // ❌ BREAKS EVERYTHING!
const Hero: IBlockswebComponent = ({ title }) => {
return <h1>{title}</h1>;
};What breaks:
- ❌ Editor cannot render component in iframe
- ❌ Component selection doesn't work
- ❌ Props updates don't reflect
- ❌ Auto-save fails
- ❌ 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:
// ✅ 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.tsxfiles 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
- Creating Components - Build with this pattern
- Options System - How props flow from editor
- Utilities - RichText, Image, BlockOutlet