Skip to content

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

bash
touch components/FeatureSection.tsx

3. Define Types

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

type FeatureSectionProps = {
  title: string;
  description: string;
  backgroundColor: string;
  features: any[]; // Will be nested components
};

4. Build the Component

typescript
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

typescript
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

typescript
// blocksweb.config.ts
import FeatureSection from './components/FeatureSection';

export const editorComponents = [
  FeatureSection,
  // ... other components
];

7. Test in Editor

  1. Start dev server: npm run dev
  2. Open editor
  3. Add your component
  4. Test all options work

Common Patterns

Responsive Design

typescript
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  {/* Content */}
</div>

Conditional Rendering

typescript
{showButton && (
  <a href={buttonUrl} className="btn">
    {buttonText}
  </a>
)}

Dynamic Classes

typescript
const alignmentClasses = {
  left: 'text-left',
  center: 'text-center',
  right: 'text-right'
};

<div className={alignmentClasses[alignment]}>
  {/* Content */}
</div>

Mapping Data

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

Advanced Techniques

Using Collections

typescript
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

typescript
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

typescript
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.tsx

Use shared UI in BlocksWeb components:

typescript
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:

typescript
// 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:

typescript
// 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;
typescript
// 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

typescript
// 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;
typescript
// 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:

typescript
'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:

typescript
const Hero: IBlockswebComponent = ({ title }) => {
  const [isVisible, setIsVisible] = useState(true);  // ❌ Error!

  return <h1>{title}</h1>;
};

✅ Correct approach:

typescript
// 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