Builder editor requires manual refresh to see the updated inputs

I have the following code:

  {

    name: 'enableManualSelection',

friendlyName: 'Enable manual selection',

helperText:

'Enable manual selection of content. By default, the content is automatically selected based on the model type, and fetched from the latest published content.',

type: 'boolean',

defaultValue: false,

  },

  {

name: 'postCount',

friendlyName: 'Post count',

helperText: 'Please refresh the page after changing the post count to see the change in the preview.',

type: 'enum',

enum: RELATED_CONTENT_POST_COUNTS,

defaultValue: '3',

showIf: "options.get('enableManualSelection') === false",

  },

  {

name: 'modelType',

friendlyName: 'Post model type',

helperText: 'Please refresh the page after changing the model type to see the change in the preview.',

type: 'string',

enum: Object.entries(RELATED_CONTENT_MODEL_TYPES).map(([key, value]) => ({ label: value, value: key })),

defaultValue: 'blog-detail',

  },

  {

name: 'cards',

type: 'list',

showIf: "options.get('modelType') === 'blog-detail' && options.get('enableManualSelection') === true",

helperText: 'You can manually select the cards to display in the Related Content.',

subFields: [

      {

name: 'card',

type: 'reference',

friendlyName: 'Blog Card',

model: 'blog-detail',

meta: {

title: 'Blog Detail',

        },

      },

],

  },…

where changing the modeType should switch what content would get fetch.
In the Builder editor, I have to manually refresh the page to see the change.
Is there a way to detect the change, or state without the need to manually refreshing the page.

Builder content link
Here is the builder link.

Builder public api key
1d8ecee591ac4358befb8fe998100548

What are you trying to accomplish
e.g. I want to integrate builder on a landing page

Screenshots or video link
Here is the loom link

Code stack you are integrating Builder with
NextJs

Hello @tlee-pb,

We suspect the issue may be related to the integration. Could you share the complete code for your Related Content custom component so we can take a closer look?

Additionally, please confirm which Builder SDK and SDK version you’re currently using.

Thanks,

Hello @manish-sharma

Here are our relatedContent code:

Presenter (index):

import React, { useMemo } from 'react';

import RelatedContent from '@/components/RelatedContent';

import type { ArticleCardProps } from '@/components/ArticleCard/consts';

import type {

RelatedContentModelType,

RelatedContentPostCount,

RelatedContentCardItem,

} from '@/builder-presenters/RelatedContent/consts';

import { type BasePresenterProps, builderPresenter } from '@/utils/builder/builderPresenter';

import type { HeadingWithEyebrowProps } from '@/molecules/HeadingWithEyebrow/consts';

import SectionNew from '@/molecules/SectionNew';

import type { SectionNewGap, SectionNewPosition, SectionNewProps } from '@/molecules/SectionNew/consts';

import type { SectionPresenterProps } from '@/builder-presenters/Section';

import { getRelatedContentAsyncProps, transformToArticleCard } from '@/builder-presenters/RelatedContent/utils';




// Raw section props from Builder.io (derived from SectionPresenterProps, omitting BasePresenterProps, and required fields)

// Position and gap are made optional for this use case

// Align is filtered out from the Builder inputs

type RawSectionProps = Omit<SectionPresenterProps, keyof BasePresenterProps | 'position' | 'gap' | 'align'> & {

position?: SectionNewPosition;

gap?: SectionNewGap;

};




// Transform raw Builder.io section props to SectionNew props

// This is needed because Builder uses 'image' but SectionNew expects 'backgroundImage'

const transformSectionProps = (section?: RawSectionProps): SectionNewProps | undefined => {

if (!section) return undefined;




const { image, useImageBackground, useVideoBackground, video, useGradient, ...rest } = section;




return {

...rest,

// Map Builder's 'image' to SectionNew's 'backgroundImage'

...(useImageBackground && image ? { backgroundImage: image } : {}),

// Map Builder's 'video' to SectionNew's 'videoSources'

...(useVideoBackground && video

? {

videoSources: {

small: video.srcSmall || '',

medium: video.srcMedium || '',

large: video.srcLarge || '',

          },

        }

: {}),

// Only include gradient if useGradient is true

...(useGradient && rest.gradient ? { gradient: rest.gradient } : {}),

  } as SectionNewProps;

};




export type RelatedContentAsyncProps = {

modelType: RelatedContentModelType;

postCount: RelatedContentPostCount;

data: ArticleCardProps[];

heading?: HeadingWithEyebrowProps;

section?: RawSectionProps;

};




export type RelatedContentPresenterProps = BasePresenterProps &

RelatedContentAsyncProps & {

enableManualSelection: boolean;

cards?: RelatedContentCardItem[];

  };




type TransformedProps =

| {

enableManualSelection: false;

data: ArticleCardProps[];

heading?: HeadingWithEyebrowProps;

section?: SectionNewProps;

    }

| {

enableManualSelection: true;

heading?: HeadingWithEyebrowProps;

section?: SectionNewProps;

cards: RelatedContentCardItem[];

    };




export const transformRelatedContentProps = ({

enableManualSelection,

data,

builderBlock,

heading,

section,

...props

}: RelatedContentPresenterProps): TransformedProps => {

// Transform section props from Builder.io format to SectionNew format

const transformedSection = transformSectionProps(section);




if (!enableManualSelection) {

return {

enableManualSelection: false,

data,

heading,

section: transformedSection,

    };

  }




// Extract cards directly from builderBlock to avoid async props interference

// This ensures we always get the latest cards from Builder, even when async props cause re-renders

const cards = (builderBlock?.component?.options?.cards as RelatedContentCardItem[] | undefined) || props.cards || [];




return {

enableManualSelection: true,

heading,

section: transformedSection,

cards,

  };

};




export default builderPresenter(

props => {

const { enableManualSelection, section, heading, ...rest } = props;

// Remove data and cards from rest to prevent them from being spread onto SectionNew

// eslint-disable-next-line @typescript-eslint/no-unused-vars

const { data, cards, ...sectionProps } = rest as typeof rest & { data?: unknown; cards?: unknown };




if (!enableManualSelection) {

return (

<SectionNew {...section} {...sectionProps}>

<RelatedContent data={props.data} heading={heading} />

</SectionNew>

      );

    }




// Use useMemo to stabilize the manualCards array and prevent unnecessary re-renders

// Use a ref to preserve cards across renders to prevent them from disappearing

const previousCardsRef = React.useRef<ArticleCardProps[]>([]);




// Create a stable dependency string based on card IDs instead of array reference

// This ensures memoization only recalculates when card content actually changes

// We compute the string inline in the dependency array since React compares values, not references

const cardIdsString =

props.cards

        ?.map(card => card?.card?.id)

        .filter((id): id is string => Boolean(id))

        .join(',') ?? '';




const manualCards: ArticleCardProps[] = useMemo(() => {

if (!props.cards || props.cards.length === 0) {

// If we have previous cards, keep them to prevent disappearing

// This handles the case where props.cards temporarily becomes empty during re-renders

if (previousCardsRef.current.length > 0) {

return previousCardsRef.current;

        }

return [];

      }




// Check if cards have valid content - if not, use previous cards

const cardsWithValidContent = props.cards.filter(card => card?.card?.value);

const validCardsCount = cardsWithValidContent.length;




// If we have previous cards but current cards are invalid, keep previous cards

if (validCardsCount === 0 && previousCardsRef.current.length > 0) {

return previousCardsRef.current;

      }




const transformed = props.cards

        .map(card => {

const content = card?.card?.value;

if (!content) {

return null;

          }

// Extract model type from BuilderReference if available

const modelType = card?.card?.model;

return transformToArticleCard(content, modelType);

        })

        .filter((item): item is ArticleCardProps => item !== null);




// If transformation resulted in fewer cards than we had before, and we have previous cards, use previous

if (transformed.length === 0 && previousCardsRef.current.length > 0) {

return previousCardsRef.current;

      }




// Update the ref with the new cards only if we got valid cards

if (transformed.length > 0) {

previousCardsRef.current = transformed;

      }




return transformed.length > 0 ? transformed : previousCardsRef.current;

// eslint-disable-next-line react-hooks/exhaustive-deps

    }, [cardIdsString]);




return (

<SectionNew {...section} {...sectionProps}>

<RelatedContent data={manualCards} heading={heading} />

</SectionNew>

    );

  },

transformRelatedContentProps,

  { includeSection: false, fetchAsyncProps: getRelatedContentAsyncProps },

);






return (

<RelatedContentWrapper ref={ref} {...rest}>

<HeadingWithEyebrow type="heading2" align="center" {...headingProps}>

{headingChildren}

</HeadingWithEyebrow>

<CardWrapper>

{data ? (

data.map(blog => {

return <StyledArticleCard key={`RelatedContent_${blog.title}`} variant="boxed" {...blog} />;

          })

        ) : (

<Spinner size="xl" />

        )}

</CardWrapper>

</RelatedContentWrapper>

  );

};

export default memo(forwardRef(RelatedContent));

Present (Builder config)

import type { Component, Input } from '@builder.io/sdk';

import { HeadingInputs } from '@/builder-presenters/Heading/heading.builder';

import { RELATED_CONTENT_MODEL_TYPES, RELATED_CONTENT_POST_COUNTS } from '@/builder-presenters/RelatedContent/consts';

import { SectionInputs } from '@/builder-presenters/Section/section.builder';




const relatedContentInputs: Input[] = [

  {

name: 'heading',

friendlyName: 'Heading',

type: 'object',

subFields: HeadingInputs.filter(field => !['align'].includes(field.name) && field.name !== 'eyebrow'),

defaultValue: {

type: 'heading2',

children: 'You might also like',

    },

  },

  {

name: 'enableManualSelection',

friendlyName: 'Enable manual selection',

helperText:

'Enable manual selection of content. By default, the content is automatically selected based on the model type, and fetched from the latest published content.',

type: 'boolean',

defaultValue: false,

  },

  {

name: 'postCount',

friendlyName: 'Post count',

helperText: 'Please refresh the page after changing the post count to see the change in the preview.',

type: 'enum',

enum: RELATED_CONTENT_POST_COUNTS.map(count => ({ label: count.toString(), value: count })),

defaultValue: RELATED_CONTENT_POST_COUNTS[0],

showIf: "options.get('enableManualSelection') === false",

  },

  {

name: 'modelType',

friendlyName: 'Post model type',

helperText: 'Please refresh the page after changing the model type to see the change in the preview.',

type: 'string',

enum: Object.entries(RELATED_CONTENT_MODEL_TYPES).map(([key, value]) => ({ label: value, value: key })),

defaultValue: 'blog-detail',

  },

// Builder doesn't seem to allow dynamic fields based on the model type, so we have to use a list of cards for each model type.

// I submitted an inquiry to Builder.io to see if this is possible, but no response yet.

// 


  {

name: 'cards',

type: 'list',

showIf: "options.get('modelType') === 'blog-detail' && options.get('enableManualSelection') === true",

helperText: 'You can manually select the cards to display in the Related Content.',

subFields: [

      {

name: 'card',

type: 'reference',

friendlyName: 'Blog Card',

model: 'blog-detail',

meta: {

title: 'Blog Detail',

        },

      },

],

  },

  {

name: 'cards',

type: 'list',

showIf: "options.get('modelType') === 'event-detail' && options.get('enableManualSelection') === true",

helperText: 'You can manually select the cards to display in the Related Content.',

subFields: [

      {

name: 'card',

friendlyName: 'Event Card',

type: 'reference',

model: 'event-detail',

meta: {

title: 'Event Detail',

        },

      },

],

  },

  {

name: 'cards',

type: 'list',

showIf: "options.get('modelType') === 'customer-detail' && options.get('enableManualSelection') === true",

helperText: 'You can manually select the cards to display in the Related Content.',

subFields: [

      {

name: 'card',

friendlyName: 'Customer Card',

type: 'reference',

model: 'customer-detail',

meta: {

title: 'Customer Detail',

        },

      },

],

  },

  {

name: 'cards',

type: 'list',

showIf: "options.get('modelType') === 'ebook-detail' && options.get('enableManualSelection') === true",

helperText: 'You can manually select the cards to display in the Related Content.',

subFields: [

      {

name: 'card',

friendlyName: 'Ebook Card',

type: 'reference',

model: 'ebook-detail',

meta: {

title: 'Ebook Detail',

        },

      },

],

  },

  {

name: 'cards',

type: 'list',

showIf: "options.get('modelType') === 'resources-infographic' && options.get('enableManualSelection') === true",

helperText: 'You can manually select the cards to display in the Related Content.',

subFields: [

      {

name: 'card',

friendlyName: 'Infographic Card',

type: 'reference',

model: 'resources-infographic',

meta: {

title: 'Infographic Detail',

        },

      },

],

  },

  {

name: 'cards',

type: 'list',

showIf: "options.get('modelType') === 'resources-product-demo' && options.get('enableManualSelection') === true",

helperText: 'You can manually select the cards to display in the Related Content.',

subFields: [

      {

name: 'card',

friendlyName: 'Product Demo Card',

type: 'reference',

model: 'resources-product-demo',

meta: {

title: 'Product Demo Detail',

        },

      },

],

  },

  {

name: 'cards',

friendlyName: 'List of Cards',

type: 'list',

showIf: "options.get('modelType') === 'resources-webinar' && options.get('enableManualSelection') === true",

helperText: 'You can manually select the cards to display in the Related Content.',

subFields: [

      {

name: 'card',

friendlyName: 'Webinar Card',

type: 'reference',

model: 'resources-webinar',

meta: {

title: 'Webinar Detail',

        },

      },

],

  },

  {

name: 'section',

type: 'object',

subFields: SectionInputs.filter(field => field.name !== 'align'),

advanced: true,

  },

];




export const RelatedContentComponent: Component = {

name: 'Related Content',

noWrap: true,

image:

'https://cdn.builder.io/api/v1/image/assets%2F1d8ecee591ac4358befb8fe998100548%2F82b8f6b8aa224e1aade0684a398a3133',

inputs: relatedContentInputs,

canHaveChildren: true,

};

Component:

import type { ForwardRefRenderFunction } from 'react';

import { forwardRef, memo } from 'react';

import HeadingWithEyebrow from '@/molecules/HeadingWithEyebrow';

import type { RelatedContentProps } from '@/components/RelatedContent/consts';

import { CardWrapper, RelatedContentWrapper, StyledArticleCard } from '@/components/RelatedContent/styled';

import { Spinner } from '@/molecules/Spinner';




const RelatedContent: ForwardRefRenderFunction<HTMLDivElement, RelatedContentProps> = (

  { heading, data, ...rest }: RelatedContentProps,

ref,

) => {

const { children: headingChildren = 'You might also like', ...headingProps } = heading || {};




return (

<RelatedContentWrapper ref={ref} {...rest}>

<HeadingWithEyebrow type="heading2" align="center" {...headingProps}>

{headingChildren}

</HeadingWithEyebrow>

<CardWrapper>

{data ? (

data.map(blog => {

return <StyledArticleCard key={`RelatedContent_${blog.title}`} variant="boxed" {...blog} />;

          })

        ) : (

<Spinner size="xl" />

        )}

</CardWrapper>

</RelatedContentWrapper>

  );

};

export default memo(forwardRef(RelatedContent));