Step 4 of 21 (19% complete)
Implementing Block Factory Mapper
Step Code
The code for this specific step can be found on the following branch:
The Block Factory Mapper pattern helps render different content blocks dynamically in your Next.js application. This pattern is crucial for creating flexible and maintainable content-driven websites.
Understanding the Block Factory Mapper
The Block Factory Mapper is a design pattern that allows for dynamic rendering of different types of content blocks. It's particularly useful when working with a headless CMS like Optimizely, where the structure and types of content can vary significantly.
Key Components
- ContentAreaMapper: Renders an array of content blocks
- Block Component: Connects the mapper to individual block components
- Block Factory: Creates the right component based on block type
Implementation Steps
1. Create the Block Factory
The Block Factory is where the magic happens.
// lib/utils/block-factory.ts import { createElement, ComponentType } from 'react' type ComponentMap = Record<string, ComponentType<any>> export default function blocksMapperFactory<TMap extends ComponentMap>( contentTypeMap: TMap ) { function factory<TypeName extends keyof TMap>({ typeName, props, }: { typeName: TypeName props: React.ComponentProps<TMap[TypeName]> }) { const Component = contentTypeMap[typeName] if (!Component) { return null } return createElement(Component, props) } return factory }
This factory function is a masterpiece of flexibility:
- The
blocksMapperFactory
function returns afactory
function that can create any type of block component based on thetypeName
. - It leverages React's
createElement
function to dynamically instantiate components, allowing for maximum flexibility in component creation. - The function includes a null check to gracefully handle cases where a component type is not found in the
contentTypeMap
.
2. Set Up Block Components Registry
The Block
component serves as the bridge between our ContentAreaMapper and the individual block components:
// components/content-area/block.tsx import dynamic from 'next/dynamic'; import blocksMapperFactory from '@/lib/utils/block-factory'; // Dynamically import each block const ContactBlock = dynamic(() => import('../block/contact-block')); const HeroBlock = dynamic(() => import('../block/hero-block')); const LogosBlock = dynamic(() => import('../block/logos-block')); const PortfolioGridBlock = dynamic(() => import('../block/portfolio-grid-block')); const ServicesBlock = dynamic(() => import('../block/services-block')); const TestimonialsBlock = dynamic(() => import('../block/testimonials-block')); // Map the dynamically imported export const blocks = { ContactBlock, HeroBlock, LogosBlock, PortfolioGridBlock, ServicesBlock, TestimonialsBlock } as const; export default blocksMapperFactory(blocks);
It uses Next.js's dynamic
function to implement code splitting, ensuring that block components are loaded only when needed.
3. Create the ContentAreaMapper
The ContentAreaMapper
component is the heart of our block rendering system.
// components/content-area/mapper.tsx import Block from './block'; function ContentAreaMapper({ blocks, preview = false }: { blocks?: any[] | null; preview?: boolean; }) { if (!blocks || blocks.length === 0) return null; return ( <> {blocks?.map(({ __typename, ...props }, index) => ( <Block key={`${__typename satisfies string}--${index}`} typeName={__typename} props={{ ...props, isFirst: index === 0, preview }} /> ))} </> ); } export default ContentAreaMapper;
This component is designed with flexibility in mind:
- It accepts an array of
blocks
and apreview
flag as props. - It maps over each block, extracting the crucial
__typename
property, which serves as the identifier for the block type. - The remaining properties are passed to the
Block
component, along with additional data likeisFirst
(useful for preloading the first image on the page) and thepreview
status.
4. Example of use on the page
// app/page.tsx import { Suspense } from 'react'; import { optimizely } from 'lib/optimizely/fetch'; import ContentAreaMapper from 'components/content-area-mapper'; export default async function HomePage() { const pageResponse = await optimizely.GetStartPage(); const startPage = pageResponse.data?.StartPage?.items?.[0]; const blocks = (startPage?.Blocks ?? []).filter((block) => block !== null && block !== undefined); return ( <> <Suspense> <ContentAreaMapper blocks={blocks} /> </Suspense> </> ); }
Benefits
- Flexibility: Easily add or remove block types
- Performance: Only load the blocks you need with dynamic imports
- Maintainability: Each block is isolated and independently updatable
Conclusion
This pattern works well for content-driven websites, especially when using a headless CMS like Optimizely because the pattern creates a scalable, performant, and maintainable codebase that can effortlessly adapt to evolving content requirements.
As your content model grows and changes, you may need to refine existing block components or introduce new ones. However, the robust structure provided by this pattern ensures that such changes remain straightforward and manageable.
Have questions? I'm here to help!