Skip to content

EnableEditor state merging breaks reactivity of blocks in Qwik #4191

@intellix

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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions