Home
Frontend

Optimizing Block-Based Content Rendering in Next.js with Headless CMS

2 minute read
#Next.Js
#Architecture
#Sanity.io
#Optimization
#React

When we have built content-heavy websites with Next.js and a headless CMS (Sanity, in our case), one common challenge in all of the projects is rendering dynamic content blocks without bloating the website with JavaScript. Here's a scalable pattern for handling block-based content with proper typing and dynamic imports.

The Architecture

The solution consists of three main parts:

  • A root component that renders all blocks
  • A specialized block renderer with dynamic imports
  • Individual block components with consistent typing

1. The Root Block Renderer

tsx

type BlocksProps = {
  blocks: Block[];
  textProps?: TextPassThroughProps;
  isText?: boolean;
  className?: string;
};

export default function BlockRenderer({
  blocks,
  textProps,
  isText,
  className,
}: BlocksProps) {
  return (
    <BlockContext
      textProps={textProps}
      isText={isText}
      className={className}
    >
      <div className={clsx('content-wrapper', className)}>
        {blocks.map((block) => {
          const key = `${block._type}-${block._key}`;
          return (
            <Fragment key={key}>
              <div data-block-type={block._type} style={{ display: 'none' }} />
              <BlockComponent block={block} useContainer={useContainer} />
            </Fragment>
          );
        })}
      </div>
    </BlockContext>
  );
}

2. Dynamic Block Component Registry

tsx

type ComponentProps<T extends Block> = {
  block: T;
  useContainer?: boolean;
};

type BlockComponent<T extends Block> = React.ComponentType<ComponentProps<T>>;

const blockComponents = {
  hero: dynamic(() => import('./blocks/hero')),
  text: dynamic(() => import('./blocks/text')),
  media: dynamic(() => import('./blocks/media')),
  gallery: dynamic(() => import('./blocks/gallery')),
  form: dynamic(() => import('./blocks/form')),
  // ... other block components
};

function BlockComponent(props: ComponentProps<Block>) {
  const Component = blockComponents[props.block._type as keyof typeof blockComponents];
  if (!Component) return null;
  return <Component {...props} />;
}

export default memo(BlockComponent);

3. Individual Block Components

tsx

export default function TextBlock(props: ComponentProps<ITextBlock>) {
  return (
    <Container useContainer={props.useContainer}>
      <Text {...props.block} />
    </Container>
  );
}

Type System

The type system ensures type safety across all blocks:

typescript

// Base block types
export type Block = 
  | IHeroBlock
  | ITextBlock
  | IMediaBlock
  | IGalleryBlock
  | IFormBlock;

// Utility types for working with blocks
export type BlockTypeMap = {
  [B in Block as B['_type']]: B;
};

export type BlockByType<T extends Block['_type']> = BlockTypeMap[T];

export const isBlockType = <T extends Block['_type']>(
  block: Block,
  type: T
): block is BlockByType<T> => block._type === type;

export type BlockType = Block['_type'];

Why This Works Well

  • Code Splitting - Components load on demand, keeping initial page loads light
  • Type Safety - TypeScript catches errors early and provides good autocomplete
  • Easy Updates - New blocks just need a component and type definition
  • Better Performance - Prevents unnecessary re-renders with memo
  • Flexible Layout - Adapts to different designs through props

Tips

  • Use ⁠_type and ⁠_key for block IDs
  • Keep TypeScript interfaces up to date
  • Use dynamic imports for larger blocks
  • Add error boundaries
  • Include loading states for better UX

In Short

This pattern works well for content-heavy sites. It's maintainable, performs well, and scales nicely as your content model grows. Adapt it to your needs, but keep the core concepts of type safety and code splitting.

Credits

Special thanks to Nicklas for his valuable contributions in developing and refining this block-based rendering pattern.

Martin Kulvedrøsten Myhre

Martin Kulvedrøsten Myhre

Fullstack developer & junior consultant at Inmeta Consoulting AS. CEO and founder of Limeyfy AS and 3Steps AS.