Guides
Render Dynamic Zones

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:

  1. Hero section
  2. Feature grid
  3. CTA banner
  4. Testimonials

Another page might have:

  1. Hero section
  2. CTA banner
  3. Video section

Step 1: Create Components in Strapi

1. Create Hero Component

Content-Type Builder → Create component → sections.hero:

FieldTypeRequired
titleTextYes
subtitleText (Long)No
imageMedia (Single)No
buttonsComponent (Repeatable)No

2. Create CTA Component

Component → sections.cta:

FieldTypeRequired
textText (Long)Yes
buttonComponent (Single)Yes

3. Create Features Component

Component → sections.features:

FieldTypeRequired
titleTextYes
featuresComponent (Repeatable)Yes

4. Create Button Component

Component → elements.button:

FieldTypeRequired
labelTextYes
hrefTextYes
variantEnumerationNo

Variant options: primary, secondary, outline

Step 2: Create Dynamic Zone

In your Page content type:

  1. Add field → Dynamic Zone
  2. Name it: sections
  3. Add components to the zone:
    • sections.hero
    • sections.cta
    • sections.features
    • Any others you created

Step 3: Create GraphQL Query

graphql/queries/getPage.graphql
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 codegen

This creates types like:

type PageSections = Array<
  | ComponentSectionsHero
  | ComponentSectionsCta
  | ComponentSectionsFeatures
>;

Step 5: Create React Components

Hero Section

components/sections/Hero.tsx
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

components/sections/Cta.tsx
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

components/sections/Features.tsx
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

lib/component-map.tsx
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

app/[slug]/page.tsx
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-us

3. 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:

  1. Check __typename in GraphQL query
  2. Verify __typename matches component map key exactly
  3. Check component is imported and exported
  4. Look for console errors

TypeScript Errors

Problem: Type errors on component props

Solutions:

  1. Run npm run codegen
  2. Import types from graphql/generated.ts
  3. Check GraphQL query includes all fields used in component

Wrong Component Rendered

Problem: Different component appears than expected

Solutions:

  1. Verify __typename in browser DevTools
  2. Check component map keys match
  3. Ensure components are in correct order in Strapi

Missing Fields

Problem: Some data doesn't appear

Solutions:

  1. Check GraphQL query includes the field
  2. Run npm run codegen after adding fields
  3. Verify field has value in Strapi
  4. Check for null/undefined in component

Best Practices

1. Consistent Naming

Strapi: sections.hero
GraphQL: ComponentSectionsHero
React: HeroSection
File: Hero.tsx

2. 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

See Also


GPL-3.0 2025 © fuqom.