Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion packages/react/lib/on-change.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
var PATH_SEPARATOR = '.';
var TARGET = Symbol('target');
var UNSUBSCRIBE = Symbol('unsubscribe');
var SUSPEND = Symbol('suspend');
var RESUME = Symbol('resume');
var isPrimitive = function (value) {
return value === null || (typeof value !== 'object' && typeof value !== 'function');
};
Expand Down Expand Up @@ -56,6 +58,7 @@ var onChange = function (object, onChange, options) {
var applyPath;
var applyPrevious;
var isUnsubscribed = false;
var isSuspended = false;
var equals = options.equals || Object.is;
var propCache = new WeakMap();
var pathCache = new WeakMap();
Expand Down Expand Up @@ -126,8 +129,18 @@ var onChange = function (object, onChange, options) {
proxyCache = null;
return target;
};
var suspend = function () {
isSuspended = true;
};
var resume = function () {
isSuspended = false;
};
var ignoreChange = function (property) {
return isUnsubscribed || (options.ignoreSymbols === true && typeof property === 'symbol');
return (
isUnsubscribed ||
isSuspended ||
(options.ignoreSymbols === true && typeof property === 'symbol')
);
};
var handler = {
get: function (target, property, receiver) {
Expand All @@ -137,6 +150,12 @@ var onChange = function (object, onChange, options) {
if (property === UNSUBSCRIBE && pathCache.get(target) === '') {
return unsubscribe(target);
}
if (property === SUSPEND && pathCache.get(target) === '') {
return suspend;
}
if (property === RESUME && pathCache.get(target) === '') {
return resume;
}
var value = Reflect.get(target, property, receiver);
if (
isPrimitive(value) ||
Expand Down Expand Up @@ -228,5 +247,19 @@ onChange.target = function (proxy) {
onChange.unsubscribe = function (proxy) {
return proxy[UNSUBSCRIBE] || proxy;
};
onChange.suspend = function (proxy) {
var suspendFn = proxy[SUSPEND];
if (suspendFn) {
suspendFn();
}
return proxy;
};
onChange.resume = function (proxy) {
var resumeFn = proxy[RESUME];
if (resumeFn) {
resumeFn();
}
return proxy;
};
module.exports = onChange;
exports.default = onChange;
45 changes: 33 additions & 12 deletions packages/react/src/components/builder-component.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -568,27 +568,46 @@ export class BuilderComponent extends React.Component<
case 'builder.resetState': {
const { state, model } = info.data;
if (model === this.name) {
for (const key in this.rootState) {
// TODO: support nested functions (somehow)
if (typeof this.rootState[key] !== 'function') {
delete this.rootState[key];
// Suspend change tracking to batch all updates
onChange.suspend(this.rootState);

try {
for (const key in this.rootState) {
// TODO: support nested functions (somehow)
if (typeof this.rootState[key] !== 'function') {
delete this.rootState[key];
}
}

Object.assign(this.rootState, state);
this.setState({
...this.state,
state: this.rootState,
updates: ((this.state && this.state.updates) || 0) + 1,
});
} finally {
// Resume change tracking - ensure this always runs even if deletion fails
onChange.resume(this.rootState);
this.debouncedUpdateState();
}
Object.assign(this.rootState, state);
this.setState({
...this.state,
state: this.rootState,
updates: ((this.state && this.state.updates) || 0) + 1,
});
}
break;
}
case 'builder.resetSymbolState': {
const { state, model, id } = info.data.state;
if (this.props.builderBlock && this.props.builderBlock === id) {
for (const key in this.rootState) {
delete this.rootState[key];
// Suspend change tracking to batch all updates
onChange.suspend(this.rootState);

try {
for (const key in this.rootState) {
delete this.rootState[key];
}
} finally {
// Resume change tracking - ensure this always runs even if deletion fails
onChange.resume(this.rootState);
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Bug

The builder.resetSymbolState case performs state updates (Object.assign, setState) after change tracking resumes, defeating batching and risking inconsistent state if an error occurs during deletion. Separately, the builder.resetState case causes redundant re-renders by calling setState directly and then again via debouncedUpdateState.

Fix in Cursor Fix in Web

Object.assign(this.rootState, state);
this.setState({
...this.state,
Expand Down Expand Up @@ -810,6 +829,8 @@ export class BuilderComponent extends React.Component<
this.notifyStateChange();
};

debouncedUpdateState = debounce(this.updateState, 1000);

get isPreviewing() {
return (
(Builder.isServer || (Builder.isBrowser && Builder.isPreviewing && !this.firstLoad)) &&
Expand Down
Loading