How to Render Dynamic Zones
Build flexible, modular pages using Strapi's Dynamic Zones with type-safe component rendering.
Overview
Dynamic Zones allow content editors to build pages from reusable components without developer intervention. This guide shows you how to:
- ✅ Create Dynamic Zone in Strapi
- ✅ Set up components for the zone
- ✅ Query dynamic content with GraphQL
- ✅ Render components with StrapiRenderer
- ✅ Handle errors gracefully
What Are Dynamic Zones?
Dynamic Zones let editors:
- Choose which components to add
- Arrange components in any order
- Add/remove components freely
- Build unique page layouts
Example: A page might have:
- Hero section
- Feature grid
- CTA banner
- Testimonials
Another page might have:
- Hero section
- CTA banner
- Video section
Step 1: Create Components in Strapi
1. Create Hero Component
Content-Type Builder → Create component → sections.hero:
| Field | Type | Required |
|---|---|---|
title | Text | Yes |
subtitle | Text (Long) | No |
image | Media (Single) | No |
buttons | Component (Repeatable) | No |
2. Create CTA Component
Component → sections.cta:
| Field | Type | Required |
|---|---|---|
text | Text (Long) | Yes |
button | Component (Single) | Yes |
3. Create Features Component
Component → sections.features:
| Field | Type | Required |
|---|---|---|
title | Text | Yes |
features | Component (Repeatable) | Yes |
4. Create Button Component
Component → elements.button:
| Field | Type | Required |
|---|---|---|
label | Text | Yes |
href | Text | Yes |
variant | Enumeration | No |
Variant options: primary, secondary, outline
Step 2: Create Dynamic Zone
In your Page content type:
- Add field → Dynamic Zone
- Name it:
sections - Add components to the zone:
sections.herosections.ctasections.features- Any others you created
Step 3: Create GraphQL Query
query GetPage($slug: String!) {
pages(filters: { slug: { eq: $slug } }) {
data {
attributes {
title
sections {
__typename # Critical for component mapping
... on ComponentSectionsHero {
title
subtitle
image {
data {
attributes {
url
alternativeText
width
height
}
}
}
buttons {
label
href
variant
}
}
... on ComponentSectionsCta {
text
button {
label
href
variant
}
}
... on ComponentSectionsFeatures {
title
features {
title
description
icon
}
}
}
}
}
}
}Important: Include __typename for each component!
Step 4: Generate Types
npm run codegenThis creates types like:
type PageSections = Array<
| ComponentSectionsHero
| ComponentSectionsCta
| ComponentSectionsFeatures
>;Step 5: Create React Components
Hero Section
import { StrapiImage } from 'strapi-nextgen-framework';
import type { ComponentSectionsHero } from '@/graphql/generated';
export function HeroSection({ title, subtitle, image, buttons }: ComponentSectionsHero) {
return (
<section className="hero py-20 text-center">
<div className="container mx-auto px-4">
<h1 className="text-5xl font-bold mb-4">{title}</h1>
{subtitle && (
<p className="text-xl text-gray-600 mb-8">{subtitle}</p>
)}
{image && (
<div className="mb-8">
<StrapiImage
data={image.data?.attributes}
nextImageProps={{
priority: true,
className: "rounded-lg shadow-2xl mx-auto",
}}
/>
</div>
)}
{buttons && buttons.length > 0 && (
<div className="flex gap-4 justify-center">
{buttons.map((button, index) => (
<a
key={index}
href={button?.href || '#'}
className={`btn btn-${button?.variant || 'primary'}`}
>
{button?.label}
</a>
))}
</div>
)}
</div>
</section>
);
}CTA Section
import type { ComponentSectionsCta } from '@/graphql/generated';
export function CtaSection({ text, button }: ComponentSectionsCta) {
return (
<section className="cta bg-blue-500 text-white py-16">
<div className="container mx-auto px-4 text-center">
<p className="text-2xl mb-6">{text}</p>
{button && (
<a
href={button.href}
className="btn btn-white inline-block px-8 py-3 bg-white text-blue-500 rounded-lg font-semibold hover:bg-gray-100"
>
{button.label}
</a>
)}
</div>
</section>
);
}Features Section
import type { ComponentSectionsFeatures } from '@/graphql/generated';
export function FeaturesSection({ title, features }: ComponentSectionsFeatures) {
return (
<section className="features py-16">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold text-center mb-12">{title}</h2>
<div className="grid md:grid-cols-3 gap-8">
{features?.map((feature, index) => (
<div key={index} className="feature-card p-6 border rounded-lg">
{feature?.icon && (
<div className="text-4xl mb-4">{feature.icon}</div>
)}
<h3 className="text-xl font-semibold mb-2">{feature?.title}</h3>
<p className="text-gray-600">{feature?.description}</p>
</div>
))}
</div>
</div>
</section>
);
}Step 6: Create Component Map
import { HeroSection } from '@/components/sections/Hero';
import { CtaSection } from '@/components/sections/Cta';
import { FeaturesSection } from '@/components/sections/Features';
export const componentMap = {
'ComponentSectionsHero': HeroSection,
'ComponentSectionsCta': CtaSection,
'ComponentSectionsFeatures': FeaturesSection,
} as const;Important: Keys must match __typename from GraphQL exactly!
Step 7: Render Dynamic Page
import { strapi } from '@/lib/strapi';
import { StrapiRenderer } from 'strapi-nextgen-framework';
import { componentMap } from '@/lib/component-map';
import { GetPageDocument } from '@/graphql/generated';
import { notFound } from 'next/navigation';
export default async function DynamicPage({
params,
}: {
params: { slug: string };
}) {
const data = await strapi.getPage(params.slug, GetPageDocument);
const page = data.page?.data?.attributes;
if (!page) {
notFound();
}
return (
<main>
{/* Optional: Static header */}
<header className="py-8 border-b">
<div className="container mx-auto px-4">
<h1 className="text-4xl font-bold">{page.title}</h1>
</div>
</header>
{/* Dynamic sections */}
<StrapiRenderer
sections={page.sections}
componentMap={componentMap}
fallback={(typename) => (
<div className="py-8 bg-yellow-50 border border-yellow-200 rounded-lg my-4">
<div className="container mx-auto px-4 text-center">
<p className="text-yellow-800">
⚠️ Component <code className="bg-yellow-100 px-2 py-1 rounded">{typename}</code> is not yet implemented
</p>
</div>
</div>
)}
/>
</main>
);
}Advanced Patterns
Conditional Rendering
<StrapiRenderer
sections={page.sections?.filter((section) => {
// Only render published sections
return section.status === 'published';
})}
componentMap={componentMap}
/>Section Wrappers
Add consistent spacing/styling:
const wrappedComponentMap = {
'ComponentSectionsHero': (props) => (
<div className="section-wrapper mb-8">
<HeroSection {...props} />
</div>
),
'ComponentSectionsCta': (props) => (
<div className="section-wrapper mb-8">
<CtaSection {...props} />
</div>
),
};Lazy Loading Sections
import dynamic from 'next/dynamic';
const HeroSection = dynamic(() => import('@/components/sections/Hero'));
const FeaturesSection = dynamic(() => import('@/components/sections/Features'));
export const componentMap = {
'ComponentSectionsHero': HeroSection,
'ComponentSectionsFeatures': FeaturesSection,
};Analytics Tracking
const componentMapWithAnalytics = {
'ComponentSectionsHero': (props) => {
useEffect(() => {
trackEvent('section_viewed', {
type: 'hero',
title: props.title,
});
}, []);
return <HeroSection {...props} />;
},
};Type-Safe Component Map
import type { ComponentType } from 'react';
import type {
ComponentSectionsHero,
ComponentSectionsCta,
ComponentSectionsFeatures,
} from '@/graphql/generated';
type SectionComponents = {
'ComponentSectionsHero': ComponentType<ComponentSectionsHero>;
'ComponentSectionsCta': ComponentType<ComponentSectionsCta>;
'ComponentSectionsFeatures': ComponentType<ComponentSectionsFeatures>;
};
export const componentMap: SectionComponents = {
'ComponentSectionsHero': HeroSection,
'ComponentSectionsCta': CtaSection,
'ComponentSectionsFeatures': FeaturesSection,
};Testing Dynamic Zones
Unit Test
import { render, screen } from '@testing-library/react';
import { StrapiRenderer } from 'strapi-nextgen-framework';
const mockSections = [
{
__typename: 'ComponentSectionsHero',
title: 'Test Hero',
subtitle: 'Test Subtitle',
},
{
__typename: 'ComponentSectionsCta',
text: 'Test CTA',
button: { label: 'Click Me', href: '/test' },
},
];
const mockComponentMap = {
'ComponentSectionsHero': ({ title, subtitle }) => (
<div>
<h1>{title}</h1>
<p>{subtitle}</p>
</div>
),
'ComponentSectionsCta': ({ text, button }) => (
<div>
<p>{text}</p>
<a href={button.href}>{button.label}</a>
</div>
),
};
test('renders all dynamic sections', () => {
render(
<StrapiRenderer
sections={mockSections}
componentMap={mockComponentMap}
/>
);
expect(screen.getByText('Test Hero')).toBeInTheDocument();
expect(screen.getByText('Test Subtitle')).toBeInTheDocument();
expect(screen.getByText('Test CTA')).toBeInTheDocument();
expect(screen.getByText('Click Me')).toBeInTheDocument();
});E2E Test
import { test, expect } from '@playwright/test';
test('dynamic page renders all sections', async ({ page }) => {
await page.goto('/about');
// Verify hero section
await expect(page.locator('section.hero h1')).toContainText('About Us');
// Verify features section
await expect(page.locator('section.features')).toBeVisible();
await expect(page.locator('.feature-card')).toHaveCount(3);
// Verify CTA section
await expect(page.locator('section.cta')).toBeVisible();
await expect(page.locator('section.cta a')).toHaveAttribute('href', '/contact');
});Common Patterns
1. Section Backgrounds
const sectionBackgrounds = {
'ComponentSectionsHero': 'bg-gradient-to-r from-blue-500 to-purple-600',
'ComponentSectionsCta': 'bg-gray-900 text-white',
'ComponentSectionsFeatures': 'bg-white',
};
const componentMap = {
'ComponentSectionsHero': (props) => (
<div className={sectionBackgrounds['ComponentSectionsHero']}>
<HeroSection {...props} />
</div>
),
};2. Section IDs for Anchors
export function HeroSection({ title, ...props }: ComponentSectionsHero) {
const id = title.toLowerCase().replace(/\s+/g, '-');
return (
<section id={id} className="hero">
{/* ... */}
</section>
);
}
// Link to: /page#about-us3. Responsive Sections
export function FeaturesSection({ title, features }: ComponentSectionsFeatures) {
const columns = features?.length ?? 0;
const gridCols = columns <= 2 ? 'md:grid-cols-2' : 'md:grid-cols-3';
return (
<div className={`grid ${gridCols} gap-8`}>
{/* ... */}
</div>
);
}Troubleshooting
Components Not Rendering
Problem: Sections don't appear on page
Solutions:
- Check
__typenamein GraphQL query - Verify
__typenamematches component map key exactly - Check component is imported and exported
- Look for console errors
TypeScript Errors
Problem: Type errors on component props
Solutions:
- Run
npm run codegen - Import types from
graphql/generated.ts - Check GraphQL query includes all fields used in component
Wrong Component Rendered
Problem: Different component appears than expected
Solutions:
- Verify
__typenamein browser DevTools - Check component map keys match
- Ensure components are in correct order in Strapi
Missing Fields
Problem: Some data doesn't appear
Solutions:
- Check GraphQL query includes the field
- Run
npm run codegenafter adding fields - Verify field has value in Strapi
- Check for null/undefined in component
Best Practices
1. Consistent Naming
Strapi: sections.hero
GraphQL: ComponentSectionsHero
React: HeroSection
File: Hero.tsx2. Always Include Fallback
<StrapiRenderer
sections={sections}
componentMap={componentMap}
fallback={<DefaultSection />}
/>3. Type All Components
import type { ComponentSectionsHero } from '@/graphql/generated';
export function HeroSection(props: ComponentSectionsHero) {
// Fully typed
}4. Handle Optional Fields
export function HeroSection({ title, subtitle, image }: ComponentSectionsHero) {
return (
<section>
<h1>{title}</h1>
{subtitle && <p>{subtitle}</p>}
{image?.data?.attributes && <StrapiImage data={image.data.attributes} />}
</section>
);
}Next Steps
- Error Handling Guide - Handle component errors
- StrapiRenderer API - Full API docs
- SEO Metadata - Add SEO to dynamic pages