Skip to content

EnableEditor state merging breaks reactivity of blocks in Qwik #4191

Description

@intellix

In qwik, if you have 2x simple components and have them added to Builder as blocks:

  • Tab (provides a context for what is selected)
  • TabPane (reads context and does a hide/show)

You'll see that reactivity breaks when EnableEditor comes alive (you're editing in Builder or previewing). This is how the EnableEditor component is merging the new content:

https://github.com/BuilderIO/builder/blob/main/packages/sdks/src/components/content/components/enable-editor.lite.tsx#L129-L144

You can fix it by copying how Qwik REPL does a deepUpdate (tested and verified it works): https://github.com/QwikDev/qwik/blob/main/packages/docs/src/repl/ui/repl-output-update.ts

Then it's as simple as:

import { unwrapStore } from '@builder.io/qwik';

const deepUpdate = (prev: any, next: any) => {
  for (const key in next) {
    if (prev[key] && typeof next[key] === 'object' && typeof prev[key] === 'object') {
      deepUpdate(prev[key], next[key]);
    } else {
      if (unwrapStore(prev[key]) !== next[key]) {
        prev[key] = next[key];
      }
    }
  }
  if (Array.isArray(prev)) {
    if (prev.length !== next.length) {
      prev.length = next.length;
    }
  } else {
    for (const key in prev) {
      if (!(key in next)) {
        delete prev[key];
      }
    }
  }
};

mergeNewContent(newContent: BuilderContent, editType?: EditType) {
  deepUpdate(props.builderContextSignal.content, newContent);
}

And you have reactivity again in both default/editing mode.

Example of tabs with a context for which one is visible:

import { component$, createContextId, Signal, Slot, useContext, useContextProvider, useSignal } from '@builder.io/qwik';

const tabContextId = createContextId<Signal<string>>('active-tab-slug');

interface TabHeader {
  slug: string;
  text: string;
}

export const Tabs = component$(({ tabs }: { tabs: TabHeader[] }) => {
  const activeTabSlug = useSignal('one');
  useContextProvider(tabContextId, activeTabSlug);

  return (
    <div class="tabs">
      <nav>
        {tabs.map(tab => <button key={tab.slug} onClick$={() => activeTabSlug.value = tab.slug}>{tab.text}</button>)}
      </nav>

      <Slot />
    </div>
  )
});

export const TabPane = component$(({ slug }: { slug: string }) => {
  const active = useContext(tabContextId);

  return (
    <div class={{ visible: slug === active.value}}>
      <Slot />
    </div>
  )
});

And then via Builder CMS you can have blocks that provide a structure like this:

<Tabs tabs={[{ slug: "one", text: "One" }, { slug: "two", text: "Two" }]}>
  <TabPane slug='one'>
    <Text text="one content" />
  </TabPane>
  <TabPane slug='two'>
    <Text text="two content" />
  </TabPane>
</Tabs>

You won't be able to click between those tabs within Builder when editing. I think it works once but then breaks.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions