DEV Community

Cover image for Building dynamic Sanity CMS driven UI with Builder Pattern
Neetigya Chahar
Neetigya Chahar

Posted on • Edited on

Building dynamic Sanity CMS driven UI with Builder Pattern

The Builder Pattern is a creational design pattern used to construct complex objects step by step. It separates the construction of a complex object from its representation, so the same construction process can create different representations.

Whether you're crafting the perfect burger ๐Ÿ” or dynamically composing UI layouts from CMS data, the Builder Pattern shines in scenarios where plain object creation logic becomes messy.


๐Ÿง  When to Use Builder Pattern

  • When an object requires multiple steps to be created
  • When an object has many optional parameters or configurations
  • When creating complex objects with nested fields or layout trees

๐Ÿงฑ Structure of the Builder Pattern

  1. Product โ€“ The complex object to be built
  2. Builder Interface โ€“ Defines the steps for building the product
  3. ConcreteBuilder โ€“ Implements the builder interface
  4. Director (optional) โ€“ Orchestrates the building process
  5. Client โ€“ Uses the builder to construct the object

๐Ÿ” Real-World Analogy: Building a Burger

Letโ€™s start with a simple example.

1. Product

class Burger:
    def __init__(self):
        self.bun = None
        self.patty = None
        self.cheese = False
        self.lettuce = False
        self.tomato = False

    def __str__(self):
        return f"Burger with {self.bun} bun, {self.patty} patty" + \
               (", cheese" if self.cheese else "") + \
               (", lettuce" if self.lettuce else "") + \
               (", tomato" if self.tomato else "")
Enter fullscreen mode Exit fullscreen mode

2. Builder

class BurgerBuilder:
    def __init__(self):
        self.burger = Burger()

    def set_bun(self, bun):
        self.burger.bun = bun
        return self

    def set_patty(self, patty):
        self.burger.patty = patty
        return self

    def add_cheese(self):
        self.burger.cheese = True
        return self

    def add_lettuce(self):
        self.burger.lettuce = True
        return self

    def add_tomato(self):
        self.burger.tomato = True
        return self

    def build(self):
        return self.burger
Enter fullscreen mode Exit fullscreen mode

3. Client Usage

builder = BurgerBuilder()
burger = (
    builder.set_bun("Whole Wheat")
           .set_patty("Veg")
           .add_cheese()
           .add_lettuce()
           .build()
)

print(burger)
Enter fullscreen mode Exit fullscreen mode

๐Ÿ–จ Output:

Burger with Whole Wheat bun, Veg patty, cheese, lettuce
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก Benefits of the Builder Pattern

  • Cleaner and more maintainable code
  • Easy to add/remove steps in building process
  • Fluent API through method chaining
  • Useful for testing and configurability

๐Ÿง  Advanced Use Case: Building Nested UI Layouts from CMS

Now letโ€™s go beyond burgers and into frontend development.

Imagine you're using a CMS like Sanity that gives you a structured array of components โ€” but some components are layouts like HStack or VStack which themselves contain children. In this case, a flat .map() wonโ€™t work. You need a builder that can recursively build components based on their type.

Hereโ€™s an example of such data:

[
  {
    "_type": "hstack",
    "gap": 16,
    "children": [
      { "_type": "image", "src": "a.jpg" },
      { "_type": "card", "title": "Card A", "content": "..." },
      {
        "_type": "vstack",
        "gap": 8,
        "children": [
          { "_type": "text", "content": "Inside VStack" },
          { "_type": "button", "label": "Click me", "link": "/click" }
        ]
      }
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode

๐Ÿ‘จโ€๐Ÿ’ป Step-by-Step Builder in React

1. Leaf Components

export const ImageBlock = ({ src }) => <img src={src} className="rounded" />;
export const TextBlock = ({ content }) => <p>{content}</p>;
export const ButtonBlock = ({ label, link }) => (
  <a href={link} className="btn">{label}</a>
);
export const Card = ({ title, content }) => (
  <div className="card">
    <h3>{title}</h3>
    <p>{content}</p>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

2. Layout Components

export const HStack = ({ gap, children }) => (
  <div style={{ display: 'flex', gap }}>{children}</div>
);

export const VStack = ({ gap, children }) => (
  <div style={{ display: 'flex', flexDirection: 'column', gap }}>{children}</div>
);
Enter fullscreen mode Exit fullscreen mode

3. ๐Ÿ”ง Recursive Section Builder

// SectionBuilder.tsx
import { HStack, VStack } from "./Layouts";
import { ImageBlock, TextBlock, ButtonBlock, Card } from "./Blocks";

const builders = {
  image: (data) => <ImageBlock {...data} />,
  text: (data) => <TextBlock {...data} />,
  button: (data) => <ButtonBlock {...data} />,
  card: (data) => <Card {...data} />,
  hstack: (data, builder) => (
    <HStack gap={data.gap}>
      {data.children.map((child, i) => (
        <div key={i}>{builder.build(child)}</div>
      ))}
    </HStack>
  ),
  vstack: (data, builder) => (
    <VStack gap={data.gap}>
      {data.children.map((child, i) => (
        <div key={i}>{builder.build(child)}</div>
      ))}
    </VStack>
  ),
};

export const SectionBuilder = {
  build(data) {
    const builder = builders[data._type];
    if (!builder) return null;
    return builder(data, SectionBuilder); // Recursive call
  },
};
Enter fullscreen mode Exit fullscreen mode

4. Rendering a Page

export const Page = ({ sections }) => {
  return (
    <main>
      {sections.map((section, i) => (
        <div key={i}>{SectionBuilder.build(section)}</div>
      ))}
    </main>
  );
};
Enter fullscreen mode Exit fullscreen mode

โœ… Why This is a Builder Pattern

This is a Builder Pattern because:

  • It separates the construction of a complex, recursive UI tree from its representation.
  • Each block knows only how to render itself, not its siblings or parents.
  • The SectionBuilder is the Director, orchestrating rendering through specific builders.
  • You can add new types (like grid, tab, carousel) without changing existing builder logic.

๐Ÿ–ฅ๏ธ Where is the Builder Pattern Used in Backend?

The Builder Pattern isn't just for frontend UI โ€” it's heavily used in backend development too. Some common places include:

  • Query Builders: ORM tools like Prisma, Sequelize, and Hibernate let you construct complex database queries step-by-step.
  • HTTP Request Builders: Libraries like OkHttp and RestTemplateBuilder help construct configurable requests.
  • Email Builders: Services like SendGrid or Nodemailer use builders to construct emails with optional subject, body, attachments, etc.
  • Config Builders: Used in SDKs and APIs to initialize configuration objects with optional parameters.
  • Document Builders: For generating structured JSON, XML, or even PDFs in steps.

These patterns improve readability, allow optional parameters, and separate object construction logic from representation โ€” the core goals of the builder pattern.


๐Ÿ’ก Extensions and Improvements

  • Add React.lazy() and Suspense for large component blocks
  • Use CMS-driven styles via Tailwind/CSS-in-JS props
  • Add error boundaries for invalid CMS structures
  • Serialize/deserialize preview versions for internal tooling

โœจ Final Thoughts

Builder pattern is often taught with backend examples, but it's hugely powerful in frontend development, especially when dealing with dynamic, nested UI construction from content management systems.


Happy hacking! ๐Ÿš€

Top comments (0)