Creating Components
Best practices and patterns for building production-ready BlocksWeb components.
Component Checklist
Before creating a component, ask yourself:
- What is the purpose? (Hero, feature card, product grid, etc.)
- What should be editable? (Text, images, colors, layout options)
- Who will use it? (Marketers, content editors, developers)
- Where will it be used? (Homepage, product pages, blog posts)
Step-by-Step Process
1. Plan the Component
Sketch out what the component should look like and what should be editable.
Example: Feature Section
- ✅ Title (text)
- ✅ Description (rich text)
- ✅ Features array (nested components)
- ✅ Background color (color picker)
2. Create the File
touch components/FeatureSection.tsx3. Define Types
import { IBlockswebComponent } from '@blocksweb/core';
type FeatureSectionProps = {
title: string;
description: string;
backgroundColor: string;
features: any[]; // Will be nested components
};4. Build the Component
import { RichText } from '@blocksweb/core/client';
const FeatureSection: IBlockswebComponent<FeatureSectionProps> = ({
title,
description,
backgroundColor,
features
}) => {
return (
<section
className="feature-section py-16"
style={{ backgroundColor }}
>
<div className="container mx-auto px-4">
<h2 className="text-4xl font-bold mb-4">{title}</h2>
<div className="text-lg mb-8">
<RichText
propName="description"
text={description}
defaultText="<p>Discover what makes us unique</p>"
/>
</div>
<div className="grid grid-cols-3 gap-8">
{features}
</div>
</div>
</section>
);
};5. Add Schema
FeatureSection.schema = {
displayName: 'Feature Section',
category: 'Marketing',
description: 'Section highlighting key features',
options: [
{
type: 'text',
name: 'title',
label: 'Section Title',
default: 'Our Amazing Features'
},
{
type: 'richtext',
name: 'description',
label: 'Description',
default: '<p>Discover what makes us unique</p>'
},
{
type: 'color',
name: 'backgroundColor',
label: 'Background Color',
default: '#f9fafb'
},
{
type: 'component',
name: 'features',
label: 'Features',
allowedComponents: ['FeatureCard']
}
]
};
export default FeatureSection;6. Register Component
// blocksweb.config.ts
import FeatureSection from './components/FeatureSection';
export const editorComponents = [
FeatureSection,
// ... other components
];7. Test in Editor
- Start dev server:
npm run dev - Open editor
- Add your component
- Test all options work
Common Patterns
Responsive Design
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Content */}
</div>Conditional Rendering
{showButton && (
<a href={buttonUrl} className="btn">
{buttonText}
</a>
)}Dynamic Classes
const alignmentClasses = {
left: 'text-left',
center: 'text-center',
right: 'text-right'
};
<div className={alignmentClasses[alignment]}>
{/* Content */}
</div>Mapping Data
{items.map((item, index) => (
<div key={index}>
{item.title}
</div>
))}Advanced Techniques
Using Collections
import { useCollectionRecords } from '@blocksweb/core/client';
const ProductGrid: IBlockswebComponent = ({ productIds }) => {
const { data: products } = useCollectionRecords('products', productIds);
return (
<div className="grid grid-cols-3 gap-4">
{products?.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
ProductGrid.schema = {
displayName: 'Product Grid',
options: [
{
type: 'entity-multiple',
name: 'productIds',
label: 'Select Products',
collectionName: 'products'
}
]
};Custom Hooks
function useFeatureData(featureId: string) {
return useCollectionRecord('features', featureId);
}
const FeatureShowcase: IBlockswebComponent = ({ featureId }) => {
const { data: feature } = useFeatureData(featureId);
if (!feature) return <div>Loading...</div>;
return <div>{feature.name}</div>;
};Error Boundaries
import { ErrorBoundary } from 'react-error-boundary';
const MyComponent: IBlockswebComponent = (props) => {
return (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Content {...props} />
</ErrorBoundary>
);
};Component Library
Build reusable UI components:
components/
├── blocksweb/ # BlocksWeb components
│ ├── Hero.tsx
│ └── FeatureSection.tsx
└── ui/ # Shared UI components
├── Button.tsx
├── Card.tsx
└── Container.tsxUse shared UI in BlocksWeb components:
import { Button } from '@/components/ui/Button';
const CallToAction: IBlockswebComponent = ({ buttonText, buttonUrl }) => {
return (
<section>
<Button href={buttonUrl}>{buttonText}</Button>
</section>
);
};Server vs Client Components
CRITICAL
BlocksWeb components render on the SERVER by default. They CANNOT use React hooks like useState, useEffect, etc. If you need hooks, you must create a separate client component.
Never add "use client" to a BlocksWeb component - it will break!
Server Components (Default)
BlocksWeb components are server components:
// components/Hero.tsx
import { IBlockswebComponent } from '@blocksweb/core';
// ✅ This renders on the server
const Hero: IBlockswebComponent = ({ title, subtitle }) => {
return (
<section>
<h1>{title}</h1>
<p>{subtitle}</p>
</section>
);
};
Hero.schema = {
displayName: 'Hero Section',
options: [...]
};
export default Hero;What you CAN do in server components:
- Render JSX
- Use props
- Use utility components (RichText, Image, BlockOutlet)
- Server-side data fetching
What you CANNOT do:
- ❌ Use
useState,useEffect,useCallback, etc. - ❌ Use
onClick,onChange, event handlers - ❌ Use browser APIs (window, document, localStorage)
- ❌ Add
"use client"directive
Client Components (When You Need Hooks)
If you need interactivity or hooks, create a separate .client.tsx file:
// components/Counter.tsx - BlocksWeb component (server)
import { IBlockswebComponent } from '@blocksweb/core';
import { CounterClient } from './Counter.client';
type CounterProps = {
initialCount: number;
label: string;
};
const Counter: IBlockswebComponent<CounterProps> = ({ initialCount, label }) => {
return <CounterClient initialCount={initialCount} label={label} />;
};
Counter.schema = {
displayName: 'Counter',
options: [
{ type: 'number', name: 'initialCount', label: 'Initial Count', default: 0 },
{ type: 'text', name: 'label', label: 'Label', default: 'Count' }
]
};
export default Counter;// components/Counter.client.tsx - Client component (interactive)
'use client';
import { useState } from 'react';
type CounterClientProps = {
initialCount: number;
label: string;
};
export function CounterClient({ initialCount, label }: CounterClientProps) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>{label}: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}Pattern: Server Wrapper + Client Implementation
This is the recommended pattern:
Server component (BlocksWeb component):
- Receives props from editor
- Passes props to client component
- No hooks, no interactivity
Client component:
- Has
"use client"directive - Uses hooks and event handlers
- Handles interactivity
Common Example: Form Component
// components/ContactForm.tsx - Server (BlocksWeb)
import { IBlockswebComponent } from '@blocksweb/core';
import { ContactFormClient } from './ContactForm.client';
type ContactFormProps = {
title: string;
submitButtonText: string;
};
const ContactForm: IBlockswebComponent<ContactFormProps> = ({
title,
submitButtonText
}) => {
return (
<ContactFormClient
title={title}
submitButtonText={submitButtonText}
/>
);
};
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' }
]
};
export default ContactForm;// components/ContactForm.client.tsx - Client
'use client';
import { useState } from 'react';
export function ContactFormClient({ title, submitButtonText }) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
// Handle form submission
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify({ name, email, message })
});
};
return (
<form onSubmit={handleSubmit}>
<h2>{title}</h2>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Message"
/>
<button type="submit">{submitButtonText}</button>
</form>
);
}Why This Matters
Performance: Server components are faster - they render on the server and send plain HTML to the browser.
Hydration: Only client components need JavaScript in the browser.
Editor compatibility: BlocksWeb's editor expects server components for rendering in the visual editor.
Common Mistakes
❌ Adding "use client" to BlocksWeb component:
'use client'; // ❌ DON'T DO THIS!
const Hero: IBlockswebComponent = ({ title }) => {
// This will break the editor!
return <h1>{title}</h1>;
};❌ Using hooks directly in BlocksWeb component:
const Hero: IBlockswebComponent = ({ title }) => {
const [isVisible, setIsVisible] = useState(true); // ❌ Error!
return <h1>{title}</h1>;
};✅ Correct approach:
// Hero.tsx (server)
const Hero: IBlockswebComponent = ({ title }) => {
return <HeroClient title={title} />;
};
// Hero.client.tsx
'use client';
export function HeroClient({ title }) {
const [isVisible, setIsVisible] = useState(true); // ✅ Works!
return isVisible ? <h1>{title}</h1> : null;
}Next Steps
- Options Reference - All option types
- Utilities - RichText, Image, BlockOutlet helpers
- Data Flow - How props flow from editor to components