From 2b11cfdfc7ad5f04808bdfe12eeec69f6fecf0ab Mon Sep 17 00:00:00 2001
From: Dalton <daltojohnso@users.noreply.github.com>
Date: Thu, 22 Sep 2022 09:16:24 -0400
Subject: [PATCH] Remove Question's `update` method & related code (#25531)

* Remove _update from Question

* Update TimeseriesGroupingWidget

* Update ExtendedOptions

* Update SummarizeSidebar

* Update AddAggregationButton and AggreationItem

* Remove update method from MBQLClause

* Update ChartTypeSidebar

* Update GuiQueryEditor

* Update NativeQueryEditor

* Update BulkFilterModal

* Update Notebook

* Update NotebookSteps

* Update NotebookStep

* Update AggregateStep

* Update BreakoutStep

* Update DataStep

* Update ExpressionStep

* Update FilterStep

* Update JoinStep

* Update LimitStep

* Update SortStep

* Update NativeQueryButton

* Update QuestionDataSelector

* Update QuestionFilters

* Update QuestionRowCount

* Update View

* Update random doc

* Ensure we have a 'datasetQuery' in the setDatasetQuery function

* Ensure we have a 'datasetQuery' in the setDatasetQuery action
---
 frontend/src/metabase-lib/README.md           |  3 +-
 frontend/src/metabase-lib/lib/Question.ts     | 37 +------
 .../src/metabase-lib/lib/queries/Query.ts     | 13 ---
 .../lib/queries/structured/MBQLClause.ts      | 14 ---
 .../components/PartialQueryBuilder.jsx        |  5 +
 .../components/TimeseriesGroupingWidget.jsx   | 23 +++--
 .../modes/components/modes/TimeseriesMode.jsx | 11 ++-
 .../query_builder/actions/core/core.js        |  5 +
 .../components/ExtendedOptions.jsx            | 23 +++--
 .../components/GuiQueryEditor.jsx             | 20 ++--
 .../components/NativeQueryEditor.jsx          | 31 +++---
 .../query_builder/components/QueryModals.jsx  | 11 ++-
 .../BulkFilterModal/BulkFilterModal.tsx       |  6 +-
 .../components/notebook/Notebook.jsx          |  4 +-
 .../components/notebook/NotebookStep.jsx      |  2 +-
 .../components/notebook/NotebookSteps.jsx     |  8 +-
 .../notebook/steps/AggregateStep.jsx          |  6 +-
 .../notebook/steps/BreakoutStep.jsx           |  6 +-
 .../components/notebook/steps/DataStep.jsx    | 14 +--
 .../notebook/steps/ExpressionStep.jsx         | 10 +-
 .../components/notebook/steps/FilterStep.jsx  |  6 +-
 .../components/notebook/steps/JoinStep.jsx    | 97 ++++++++++---------
 .../components/notebook/steps/LimitStep.jsx   |  2 +-
 .../components/notebook/steps/SortStep.jsx    | 15 +--
 .../components/view/NativeQueryButton.jsx     | 18 ++--
 .../components/view/QuestionDataSelector.jsx  | 10 +-
 .../components/view/QuestionFilters.jsx       | 17 ++--
 .../components/view/QuestionRowCount.jsx      |  5 +-
 .../query_builder/components/view/View.jsx    | 29 ++++--
 .../View/NewQuestionView/NewQuestionView.tsx  |  5 +-
 .../components/view/ViewFooter.jsx            | 12 ++-
 .../components/view/ViewHeader.jsx            | 18 +++-
 .../view/sidebars/ChartTypeSidebar.jsx        | 14 +--
 .../AddAggregationButton.jsx                  |  8 +-
 .../AggregationItem/AggregationItem.jsx       | 10 +-
 .../SummarizeSidebar/SummarizeSidebar.jsx     | 18 +++-
 .../view/sidebars/SummarizeSidebar/utils.js   |  4 -
 .../query_builder/containers/QueryBuilder.jsx |  7 --
 38 files changed, 295 insertions(+), 252 deletions(-)
 delete mode 100644 frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/utils.js

diff --git a/frontend/src/metabase-lib/README.md b/frontend/src/metabase-lib/README.md
index 4979d46aa4f..1b789a31047 100644
--- a/frontend/src/metabase-lib/README.md
+++ b/frontend/src/metabase-lib/README.md
@@ -3,11 +3,10 @@
 - `setFoo(bar)`: returns clone of the wrapper but with the "foo" attribute to set to `bar`
 - `replace(object)`: returns clone of parent wrapper with this object replaced by `object`
 - `remove()`: returns clone of the parent wrapper with this object removed
-- `update()`: propagates current wrapper update to parent wrapper, recursively
 
 Examples:
 
-- `question().query().aggregation()[0].setDimension(dimension).update()`
+- `question().query().aggregation()[0].setDimension(dimension)`
 
 Exceptions:
 
diff --git a/frontend/src/metabase-lib/lib/Question.ts b/frontend/src/metabase-lib/lib/Question.ts
index 4f6c0c7b101..7fedba9b2fe 100644
--- a/frontend/src/metabase-lib/lib/Question.ts
+++ b/frontend/src/metabase-lib/lib/Question.ts
@@ -82,8 +82,6 @@ import {
   ALERT_TYPE_TIMESERIES_GOAL,
 } from "metabase-lib/lib/Alert";
 
-type QuestionUpdateFn = (q: Question) => Promise<void> | null | undefined;
-
 export type QuestionCreatorOpts = {
   databaseId?: DatabaseId;
   tableId?: TableId;
@@ -120,11 +118,6 @@ class QuestionInner {
    */
   _parameterValues: ParameterValues;
 
-  /**
-   * Bound update function, if any
-   */
-  _update: QuestionUpdateFn | null | undefined;
-
   /**
    * Question constructor
    */
@@ -132,7 +125,6 @@ class QuestionInner {
     card: CardObject,
     metadata?: Metadata,
     parameterValues?: ParameterValues,
-    update?: QuestionUpdateFn | null | undefined,
   ) {
     this._card = card;
     this._metadata =
@@ -146,16 +138,10 @@ class QuestionInner {
         questions: {},
       });
     this._parameterValues = parameterValues || {};
-    this._update = update;
   }
 
   clone() {
-    return new Question(
-      this._card,
-      this._metadata,
-      this._parameterValues,
-      this._update,
-    );
+    return new Question(this._card, this._metadata, this._parameterValues);
   }
 
   metadata(): Metadata {
@@ -172,27 +158,6 @@ class QuestionInner {
     return q;
   }
 
-  /**
-   * calls the passed in update function (useful for chaining) or bound update function with the question
-   * NOTE: this passes Question instead of card, unlike how Query passes dataset_query
-   */
-  update(update?: QuestionUpdateFn, ...args: any[]) {
-    // TODO: if update returns a new card, create a new Question based on that and return it
-    if (update) {
-      update(this, ...args);
-    } else if (this._update) {
-      this._update(this, ...args);
-    } else {
-      throw new Error("Question update function not provided or bound");
-    }
-  }
-
-  bindUpdate(update: QuestionUpdateFn) {
-    const q = this.clone();
-    q._update = update;
-    return q;
-  }
-
   withoutNameAndId() {
     return this.setCard(
       chain(this.card())
diff --git a/frontend/src/metabase-lib/lib/queries/Query.ts b/frontend/src/metabase-lib/lib/queries/Query.ts
index 43f6cdec7ec..0e9a68213c4 100644
--- a/frontend/src/metabase-lib/lib/queries/Query.ts
+++ b/frontend/src/metabase-lib/lib/queries/Query.ts
@@ -9,11 +9,9 @@ import Variable from "metabase-lib/lib/variables/Variable";
 import { memoizeClass } from "metabase-lib/lib/utils";
 import DimensionOptions from "metabase-lib/lib/DimensionOptions";
 
-type QueryUpdateFn = (datasetQuery: DatasetQuery) => void;
 /**
  * An abstract class for all query types (StructuredQuery & NativeQuery)
  */
-
 class QueryInner {
   _metadata: Metadata;
 
@@ -119,17 +117,6 @@ class QueryInner {
   setDefaultQuery(): QueryInner {
     return this;
   }
-
-  /**
-   * Helper for updating with functions that expect a DatasetQuery object, or proxy to parent question
-   */
-  update(update?: QueryUpdateFn, ...args: any[]) {
-    if (update) {
-      return update(this.datasetQuery(), ...args);
-    } else {
-      return this.question().update(undefined, ...args);
-    }
-  }
 }
 
 export default class Query extends memoizeClass<QueryInner>("question")(
diff --git a/frontend/src/metabase-lib/lib/queries/structured/MBQLClause.ts b/frontend/src/metabase-lib/lib/queries/structured/MBQLClause.ts
index 18d1bcf6fe2..00da3b0e6e4 100644
--- a/frontend/src/metabase-lib/lib/queries/structured/MBQLClause.ts
+++ b/frontend/src/metabase-lib/lib/queries/structured/MBQLClause.ts
@@ -49,13 +49,6 @@ export default class MBQLArrayClause extends Array {
     return this._index;
   }
 
-  /**
-   * replaces the previous clause with this one and propagates an update, recursively
-   */
-  update(...args: any) {
-    return this.replace(this).update(undefined, ...args);
-  }
-
   parent() {
     return this.replace(this);
   }
@@ -110,13 +103,6 @@ export class MBQLObjectClause {
     return this._index;
   }
 
-  /**
-   * replaces the previous clause with this one and propagates an update, recursively
-   */
-  update(...args: any) {
-    return this.replace(this).update(undefined, ...args);
-  }
-
   parent() {
     return this.replace(this);
   }
diff --git a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
index cc1d716d531..943b6f3b012 100644
--- a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
@@ -11,6 +11,7 @@ import Tables from "metabase/entities/tables";
 import GuiQueryEditor from "metabase/query_builder/components/GuiQueryEditor";
 import * as Urls from "metabase/lib/urls";
 import Question from "metabase-lib/lib/Question";
+import Query from "metabase-lib/lib/queries/Query";
 
 import withTableMetadataLoaded from "../hoc/withTableMetadataLoaded";
 
@@ -75,6 +76,10 @@ class PartialQueryBuilder extends Component {
   }
 
   setDatasetQuery = datasetQuery => {
+    if (datasetQuery instanceof Query) {
+      datasetQuery = datasetQuery.datasetQuery();
+    }
+
     this.props.onChange(datasetQuery.query);
     this.props.updatePreviewSummary(datasetQuery);
   };
diff --git a/frontend/src/metabase/modes/components/TimeseriesGroupingWidget.jsx b/frontend/src/metabase/modes/components/TimeseriesGroupingWidget.jsx
index 00aadd40b97..1caade1b9e5 100644
--- a/frontend/src/metabase/modes/components/TimeseriesGroupingWidget.jsx
+++ b/frontend/src/metabase/modes/components/TimeseriesGroupingWidget.jsx
@@ -9,20 +9,16 @@ import TimeGroupingPopover from "metabase/query_builder/components/TimeGroupingP
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
 import SelectButton from "metabase/core/components/SelectButton";
 
-// set the display automatically then run
-function updateAndRun(query) {
-  query.question().setDefaultDisplay().update(null, { run: true });
-}
-
 export default class TimeseriesGroupingWidget extends Component {
   static propTypes = {
     query: PropTypes.object.isRequired,
+    onChange: PropTypes.func.isRequired,
   };
 
   _popover;
 
   render() {
-    const { query } = this.props;
+    const { query, onChange } = this.props;
 
     if (isStructured(query.datasetQuery())) {
       const breakouts = query.breakouts();
@@ -49,9 +45,20 @@ export default class TimeseriesGroupingWidget extends Component {
                 d.isSameBaseDimension(dimension),
               );
               if (index >= 0) {
-                updateAndRun(query.updateBreakout(index, dimension.mbql()));
+                const newQuestion = query
+                  .updateBreakout(index, dimension.mbql())
+                  .question()
+                  .setDefaultDisplay();
+
+                onChange(newQuestion);
               } else {
-                updateAndRun(query.clearBreakouts().breakout(dimension.mbql()));
+                const newQuestion = query
+                  .clearBreakouts()
+                  .breakout(dimension.mbql())
+                  .question()
+                  .setDefaultDisplay();
+
+                onChange(newQuestion);
               }
               if (this._popover) {
                 this._popover.close();
diff --git a/frontend/src/metabase/modes/components/modes/TimeseriesMode.jsx b/frontend/src/metabase/modes/components/modes/TimeseriesMode.jsx
index e11f5ee9013..be4da8dff57 100644
--- a/frontend/src/metabase/modes/components/modes/TimeseriesMode.jsx
+++ b/frontend/src/metabase/modes/components/modes/TimeseriesMode.jsx
@@ -13,12 +13,21 @@ import PivotByCategoryDrill from "../drill/PivotByCategoryDrill";
 import PivotByLocationDrill from "../drill/PivotByLocationDrill";
 
 const TimeseriesModeFooter = props => {
+  const onChange = question => {
+    const { updateQuestion } = props;
+    updateQuestion(question, { run: true });
+  };
+
   return (
     <div className="flex layout-centered">
       <span className="mr1">{t`View`}</span>
       <TimeseriesFilterWidget {...props} card={props.lastRunCard} />
       <span className="mx1">{t`by`}</span>
-      <TimeseriesGroupingWidget {...props} card={props.lastRunCard} />
+      <TimeseriesGroupingWidget
+        {...props}
+        onChange={onChange}
+        card={props.lastRunCard}
+      />
     </div>
   );
 };
diff --git a/frontend/src/metabase/query_builder/actions/core/core.js b/frontend/src/metabase/query_builder/actions/core/core.js
index fc53c482d40..224235b008b 100644
--- a/frontend/src/metabase/query_builder/actions/core/core.js
+++ b/frontend/src/metabase/query_builder/actions/core/core.js
@@ -18,6 +18,7 @@ import { openUrl } from "metabase/redux/app";
 import Questions from "metabase/entities/questions";
 import Databases from "metabase/entities/databases";
 import { fetchAlertsForQuestion } from "metabase/alert/alert";
+import Query from "metabase-lib/lib/queries/Query";
 
 import { trackNewQuestionSaved } from "../../analytics";
 import {
@@ -160,6 +161,10 @@ export const navigateToNewCardInsideQB = createThunkAction(
 // DEPRECATED, still used in a couple places
 export const setDatasetQuery =
   (datasetQuery, options) => (dispatch, getState) => {
+    if (datasetQuery instanceof Query) {
+      datasetQuery = datasetQuery.datasetQuery();
+    }
+
     const question = getQuestion(getState());
     dispatch(updateQuestion(question.setDatasetQuery(datasetQuery), options));
   };
diff --git a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
index 221d228d383..1d957d20a4d 100644
--- a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
+++ b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
@@ -34,9 +34,10 @@ export class ExtendedOptionsPopover extends Component {
 
   setExpression(name, expression, previousName) {
     const { query, setDatasetQuery } = this.props;
-    query
-      .updateExpression(name, expression, previousName)
-      .update(setDatasetQuery);
+
+    const newQuery = query.updateExpression(name, expression, previousName);
+    setDatasetQuery(newQuery);
+
     this.setState({ editExpression: null });
     MetabaseAnalytics.trackStructEvent(
       "QueryBuilder",
@@ -47,7 +48,10 @@ export class ExtendedOptionsPopover extends Component {
 
   removeExpression(name) {
     const { query, setDatasetQuery } = this.props;
-    query.removeExpression(name).update(setDatasetQuery);
+
+    const newQuery = query.removeExpression(name);
+    setDatasetQuery(newQuery);
+
     this.setState({ editExpression: null });
 
     MetabaseAnalytics.trackStructEvent("QueryBuilder", "Remove Expression");
@@ -55,7 +59,10 @@ export class ExtendedOptionsPopover extends Component {
 
   setLimit = limit => {
     const { query, setDatasetQuery } = this.props;
-    query.updateLimit(limit).update(setDatasetQuery);
+
+    const newQuery = query.setLimit(limit);
+    setDatasetQuery(newQuery);
+
     MetabaseAnalytics.trackStructEvent("QueryBuilder", "Set Limit", limit);
     if (this.props.onClose) {
       this.props.onClose();
@@ -78,9 +85,9 @@ export class ExtendedOptionsPopover extends Component {
           tableMetadata={query.table()}
           sort={sort}
           fieldOptions={query.sortOptions(sort)}
-          removeOrderBy={() => query.removeSort(index).update(setDatasetQuery)}
+          removeOrderBy={() => setDatasetQuery(query.removeSort(index))}
           updateOrderBy={orderBy =>
-            query.updateSort(index, orderBy).update(setDatasetQuery)
+            setDatasetQuery(query.updateSort(index, orderBy))
           }
         />
       ));
@@ -90,7 +97,7 @@ export class ExtendedOptionsPopover extends Component {
           <AddClauseButton
             text={t`Pick a field to sort by`}
             onClick={() => {
-              query.sort(["asc", null]).update(setDatasetQuery);
+              setDatasetQuery(query.sort(["asc", null]));
             }}
           />
         );
diff --git a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
index 85de1b6f93d..beb7946429b 100644
--- a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
@@ -94,11 +94,9 @@ export default class GuiQueryEditor extends React.Component {
           <FilterWidgetList
             query={query}
             filters={filters}
-            removeFilter={index =>
-              query.removeFilter(index).update(setDatasetQuery)
-            }
+            removeFilter={index => setDatasetQuery(query.removeFilter(index))}
             updateFilter={(index, filter) =>
-              query.updateFilter(index, filter).update(setDatasetQuery)
+              setDatasetQuery(query.updateFilter(index, filter))
             }
           />
         );
@@ -135,9 +133,7 @@ export default class GuiQueryEditor extends React.Component {
             <FilterPopover
               isNew
               query={query}
-              onChangeFilter={filter =>
-                query.filter(filter).update(setDatasetQuery)
-              }
+              onChangeFilter={filter => setDatasetQuery(query.filter(filter))}
               onClose={() => this.filterPopover.current.close()}
             />
           </PopoverWithTrigger>
@@ -178,10 +174,8 @@ export default class GuiQueryEditor extends React.Component {
             query={query}
             onChangeAggregation={aggregation =>
               aggregation
-                ? query
-                    .updateAggregation(index, aggregation)
-                    .update(setDatasetQuery)
-                : query.removeAggregation(index).update(setDatasetQuery)
+                ? setDatasetQuery(query.updateAggregation(index, aggregation))
+                : setDatasetQuery(query.removeAggregation(index))
             }
             showMetrics={false}
             showRawData
@@ -241,8 +235,8 @@ export default class GuiQueryEditor extends React.Component {
           breakoutOptions={query.breakoutOptions(breakout)}
           onChangeBreakout={breakout =>
             breakout
-              ? query.updateBreakout(index, breakout).update(setDatasetQuery)
-              : query.removeBreakout(index).update(setDatasetQuery)
+              ? setDatasetQuery(query.updateBreakout(index, breakout))
+              : setDatasetQuery(query.removeBreakout(index))
           }
         >
           {this.renderAdd(index === 0 ? t`Add a grouping` : null)}
diff --git a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
index fed1b56b149..db4fabcd751 100644
--- a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
@@ -423,14 +423,15 @@ class NativeQueryEditor extends Component {
   }, AUTOCOMPLETE_DEBOUNCE_DURATION);
 
   onChange() {
-    const { query } = this.props;
+    const { query, setDatasetQuery } = this.props;
     if (this._editor && !this._localUpdate) {
       this._updateSize();
       if (query.queryText() !== this._editor.getValue()) {
-        query
-          .setQueryText(this._editor.getValue())
-          .updateSnippetsWithIds(this.props.snippets)
-          .update(this.props.setDatasetQuery);
+        setDatasetQuery(
+          query
+            .setQueryText(this._editor.getValue())
+            .updateSnippetsWithIds(this.props.snippets),
+        );
       }
     }
 
@@ -443,12 +444,9 @@ class NativeQueryEditor extends Component {
 
   /// Change the Database we're currently editing a query for.
   setDatabaseId = databaseId => {
-    const { query } = this.props;
+    const { query, setDatasetQuery } = this.props;
     if (query.databaseId() !== databaseId) {
-      query
-        .setDatabaseId(databaseId)
-        .setDefaultCollection()
-        .update(this.props.setDatasetQuery);
+      setDatasetQuery(query.setDatabaseId(databaseId).setDefaultCollection());
       if (this._editor && !this.props.readOnly) {
         // HACK: the cursor doesn't blink without this intended small delay
         setTimeout(() => this._editor.focus(), 50);
@@ -458,18 +456,16 @@ class NativeQueryEditor extends Component {
 
   setTableId = tableId => {
     // TODO: push more of this into metabase-lib?
-    const { query } = this.props;
+    const { query, setDatasetQuery } = this.props;
     const table = query.metadata().table(tableId);
     if (table?.name !== query.collection()) {
-      query.setCollectionName(table.name).update(this.props.setDatasetQuery);
+      setDatasetQuery(query.setCollectionName(table.name));
     }
   };
 
   setParameterIndex = (parameterId, parameterIndex) => {
     const { query, setDatasetQuery } = this.props;
-    query
-      .setParameterIndex(parameterId, parameterIndex)
-      .update(setDatasetQuery);
+    setDatasetQuery(query.setParameterIndex(parameterId, parameterIndex));
   };
 
   handleFilterButtonClick = () => {
@@ -492,6 +488,7 @@ class NativeQueryEditor extends Component {
       snippetCollections = [],
       resizable,
       requireWriteback = false,
+      setDatasetQuery,
     } = this.props;
 
     const parameters = query.question().parameters();
@@ -568,9 +565,7 @@ class NativeQueryEditor extends Component {
             <SnippetModal
               onSnippetUpdate={(newSnippet, oldSnippet) => {
                 if (newSnippet.name !== oldSnippet.name) {
-                  query
-                    .updateSnippetNames([newSnippet])
-                    .update(this.props.setDatasetQuery);
+                  setDatasetQuery(query.updateSnippetNames([newSnippet]));
                 }
               }}
               snippet={this.props.modalSnippet}
diff --git a/frontend/src/metabase/query_builder/components/QueryModals.jsx b/frontend/src/metabase/query_builder/components/QueryModals.jsx
index df6f6f91b60..4b7a5cf5c9d 100644
--- a/frontend/src/metabase/query_builder/components/QueryModals.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryModals.jsx
@@ -54,6 +54,11 @@ class QueryModals extends React.Component {
     }
   };
 
+  onQueryChange = query => {
+    const question = query.question();
+    this.props.updateQuestion(question, { run: true });
+  };
+
   render() {
     const {
       modal,
@@ -166,7 +171,11 @@ class QueryModals extends React.Component {
       </Modal>
     ) : modal === MODAL_TYPES.FILTERS ? (
       <Modal fit onClose={onCloseModal}>
-        <BulkFilterModal question={question} onClose={onCloseModal} />
+        <BulkFilterModal
+          question={question}
+          onQueryChange={this.onQueryChange}
+          onClose={onCloseModal}
+        />
       </Modal>
     ) : modal === MODAL_TYPES.HISTORY ? (
       <Modal onClose={onCloseModal}>
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
index cf255729bd3..affd301f008 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
@@ -36,12 +36,14 @@ import { fixBetweens, getSearchHits } from "./utils";
 
 export interface BulkFilterModalProps {
   question: Question;
+  onQueryChange: (query: StructuredQuery) => void;
   onClose?: () => void;
 }
 
 const BulkFilterModal = ({
   question,
   onClose,
+  onQueryChange,
 }: BulkFilterModalProps): JSX.Element | null => {
   const [query, setQuery] = useState(getQuery(question));
   const [isChanged, setIsChanged] = useState(false);
@@ -87,9 +89,9 @@ const BulkFilterModal = ({
 
   const handleApplyQuery = useCallback(() => {
     const preCleanedQuery = fixBetweens(query);
-    preCleanedQuery.clean().update(undefined, { run: true });
+    onQueryChange(preCleanedQuery.clean());
     onClose?.();
-  }, [query, onClose]);
+  }, [query, onClose, onQueryChange]);
 
   const clearFilters = () => {
     setQuery(query.clearFilters());
diff --git a/frontend/src/metabase/query_builder/components/notebook/Notebook.jsx b/frontend/src/metabase/query_builder/components/notebook/Notebook.jsx
index d5756487ce7..8253263799c 100644
--- a/frontend/src/metabase/query_builder/components/notebook/Notebook.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/Notebook.jsx
@@ -16,6 +16,7 @@ export default function Notebook({ className, ...props }) {
     isResultDirty,
     runQuestionQuery,
     setQueryBuilderMode,
+    updateQuestion,
     hasVisualizeButton = true,
   } = props;
 
@@ -26,7 +27,8 @@ export default function Notebook({ className, ...props }) {
     if (cleanQuestion.display() === "table") {
       cleanQuestion = cleanQuestion.setDefaultDisplay();
     }
-    await cleanQuestion.update();
+
+    await updateQuestion(cleanQuestion);
   }
 
   // vizualize switches the view to the question's visualization.
diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx b/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx
index 5a0438f500c..857e635919c 100644
--- a/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx
@@ -166,7 +166,7 @@ export default class NotebookStep extends React.Component {
               name="close"
               className="ml-auto cursor-pointer text-light text-medium-hover hover-child"
               tooltip={t`Remove`}
-              onClick={() => step.revert(step.query).update(updateQuery)}
+              onClick={() => updateQuery(step.revert(step.query))}
               data-testid="remove-step"
             />
           </StepHeader>
diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookSteps.jsx b/frontend/src/metabase/query_builder/components/notebook/NotebookSteps.jsx
index f51e92eca6f..e78904868ba 100644
--- a/frontend/src/metabase/query_builder/components/notebook/NotebookSteps.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/NotebookSteps.jsx
@@ -38,7 +38,7 @@ export default class NotebookSteps extends React.Component {
   };
 
   render() {
-    const { question, className } = this.props;
+    const { question, className, updateQuestion } = this.props;
     const { openSteps, lastOpenedStep } = this.state;
 
     if (!question) {
@@ -51,8 +51,10 @@ export default class NotebookSteps extends React.Component {
       <div className={cx(className, "pt3")}>
         {steps.map((step, index) => {
           // pass a version of updateQuery that cleans subsequent steps etc
-          const updateQuery = async datasetQuery => {
-            await step.update(datasetQuery).update();
+          const updateQuery = async query => {
+            const datasetQuery = query.datasetQuery();
+            const updatedQuery = step.update(datasetQuery);
+            await updateQuestion(updatedQuery.question());
             // mark the step as "closed" since we can assume it's been added or removed by the updateQuery
             this.closeStep(step.id);
           };
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/AggregateStep.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/AggregateStep.jsx
index 065aa1b1ab2..35647cb3152 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/AggregateStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/AggregateStep.jsx
@@ -36,13 +36,13 @@ export default function AggregateStep({
           aggregation={aggregation}
           onChangeAggregation={newAggregation =>
             aggregation
-              ? aggregation.replace(newAggregation).update(updateQuery)
-              : query.aggregate(newAggregation).update(updateQuery)
+              ? updateQuery(aggregation.replace(newAggregation))
+              : updateQuery(query.aggregate(newAggregation))
           }
         />
       )}
       isLastOpened={isLastOpened}
-      onRemove={aggregation => aggregation.remove().update(updateQuery)}
+      onRemove={aggregation => updateQuery(aggregation.remove())}
       canRemove={aggregation => aggregation.canRemove()}
     />
   );
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/BreakoutStep.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/BreakoutStep.jsx
index 8b8fa96e334..95a448262eb 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/BreakoutStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/BreakoutStep.jsx
@@ -36,13 +36,13 @@ export default function BreakoutStep({
           breakout={breakout}
           onChangeBreakout={newBreakout =>
             breakout
-              ? breakout.replace(newBreakout).update(updateQuery)
-              : query.breakout(newBreakout).update(updateQuery)
+              ? updateQuery(breakout.replace(newBreakout))
+              : updateQuery(query.breakout(newBreakout))
           }
         />
       )}
       isLastOpened={isLastOpened}
-      onRemove={breakout => breakout.remove().update(updateQuery)}
+      onRemove={breakout => updateQuery(breakout.remove())}
     />
   );
 }
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/DataStep.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/DataStep.jsx
index 0761e7aa9f4..dc8e4784487 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/DataStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/DataStep.jsx
@@ -54,7 +54,7 @@ function DataStep({ color, query, updateQuery }) {
           selectedDatabaseId={query.databaseId()}
           selectedTableId={query.tableId()}
           setSourceTableFn={tableId =>
-            query.setTableId(tableId).setDefaultQuery().update(updateQuery)
+            updateQuery(query.setTableId(tableId).setDefaultQuery())
           }
           isInitiallyOpen={!query.tableId()}
           triggerElement={
@@ -80,12 +80,12 @@ const DataFieldsPicker = ({ query, updateQuery, ...props }) => {
   const fields = query.fields();
 
   const handleSelectNone = () => {
-    query
-      .setFields([
+    updateQuery(
+      query.setFields([
         dimensions[0].mbql(),
         ...expressionDimensions.map(d => d.mbql()),
-      ])
-      .update(updateQuery);
+      ]),
+    );
   };
 
   const handleToggleDimension = dimension => {
@@ -99,7 +99,7 @@ const DataFieldsPicker = ({ query, updateQuery, ...props }) => {
       })
       .map(d => d.mbql());
 
-    query.setFields(newFields).update(updateQuery);
+    updateQuery(query.setFields(newFields));
   };
 
   const hasOneColumnSelected = fields.filter(isLocalField).length === 1;
@@ -110,7 +110,7 @@ const DataFieldsPicker = ({ query, updateQuery, ...props }) => {
       dimensions={dimensions}
       selectedDimensions={selectedDimensions}
       isAll={!fields || fields.length === 0}
-      onSelectAll={() => query.clearFields().update(updateQuery)}
+      onSelectAll={() => updateQuery(query.clearFields())}
       onSelectNone={handleSelectNone}
       disableSelected={hasOneColumnSelected}
       onToggleDimension={handleToggleDimension}
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/ExpressionStep.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/ExpressionStep.jsx
index 1ed4d2ae480..e1abb412ddd 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/ExpressionStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/ExpressionStep.jsx
@@ -23,16 +23,16 @@ export default function ExpressionStep({
           expression={expression}
           onChangeExpression={(newName, newExpression) =>
             expression
-              ? query
-                  .updateExpression(newName, newExpression, name)
-                  .update(updateQuery)
-              : query.addExpression(newName, newExpression).update(updateQuery)
+              ? updateQuery(
+                  query.updateExpression(newName, newExpression, name),
+                )
+              : updateQuery(query.addExpression(newName, newExpression))
           }
         />
       )}
       isLastOpened={isLastOpened}
       onRemove={([name, expression]) =>
-        query.removeExpression(name).update(updateQuery)
+        updateQuery(query.removeExpression(name))
       }
     />
   );
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/FilterStep.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/FilterStep.jsx
index 9987ee3e91c..73e0142e177 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/FilterStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/FilterStep.jsx
@@ -23,13 +23,13 @@ export default function FilterStep({
           filter={filter}
           onChangeFilter={newFilter =>
             filter
-              ? filter.replace(newFilter).update(updateQuery)
-              : query.filter(newFilter).update(updateQuery)
+              ? updateQuery(filter.replace(newFilter))
+              : updateQuery(query.filter(newFilter))
           }
         />
       )}
       isLastOpened={isLastOpened}
-      onRemove={filter => filter.remove().update(updateQuery)}
+      onRemove={filter => updateQuery(filter.remove())}
     />
   );
 }
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.jsx
index e63aa29273f..34ab183c389 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.jsx
@@ -95,7 +95,7 @@ export default function JoinStep({
   const valid = _.all(joins, join => join.isValid());
 
   function addNewJoinClause() {
-    query.join(new Join({ fields: "all" })).update(updateQuery);
+    updateQuery(query.join(new Join({ fields: "all" })));
   }
 
   return (
@@ -176,33 +176,35 @@ function JoinClause({ color, join, updateQuery, showRemove }) {
   }
 
   function onParentDimensionChange(index, fieldRef, { overwrite } = {}) {
-    join
-      .setParentDimension({
-        index,
-        dimension: fieldRef,
-        overwriteTemporalUnit: overwrite,
-      })
-      .setDefaultAlias()
-      .parent()
-      .update(updateQuery);
+    updateQuery(
+      join
+        .setParentDimension({
+          index,
+          dimension: fieldRef,
+          overwriteTemporalUnit: overwrite,
+        })
+        .setDefaultAlias()
+        .parent(),
+    );
     if (!join.joinDimensions()[index]) {
       joinDimensionPickersRef.current[index]?.open();
     }
   }
 
   function onJoinDimensionChange(index, fieldRef, { overwrite } = {}) {
-    join
-      .setJoinDimension({
-        index,
-        dimension: fieldRef,
-        overwriteTemporalUnit: overwrite,
-      })
-      .parent()
-      .update(updateQuery);
+    updateQuery(
+      join
+        .setJoinDimension({
+          index,
+          dimension: fieldRef,
+          overwriteTemporalUnit: overwrite,
+        })
+        .parent(),
+    );
   }
 
   function addNewDimensionsPair(index) {
-    join.addEmptyDimensionsPair().parent().update(updateQuery);
+    updateQuery(join.addEmptyDimensionsPair().parent());
 
     // Need to wait, so a new dimensions pair renders
     // and a corresponding ref is created, so we can reference it here
@@ -212,7 +214,7 @@ function JoinClause({ color, join, updateQuery, showRemove }) {
   }
 
   function removeJoin() {
-    join.remove().update(updateQuery);
+    updateQuery(join.remove());
   }
 
   return (
@@ -252,25 +254,23 @@ function JoinClause({ color, join, updateQuery, showRemove }) {
               )?.name;
 
               function removeParentDimension() {
-                join
-                  .setParentDimension({ index, dimension: null })
-                  .parent()
-                  .update(updateQuery);
+                updateQuery(
+                  join.setParentDimension({ index, dimension: null }).parent(),
+                );
               }
 
               function removeJoinDimension() {
-                join
-                  .setJoinDimension({ index, dimension: null })
-                  .parent()
-                  .update(updateQuery);
+                updateQuery(
+                  join.setJoinDimension({ index, dimension: null }).parent(),
+                );
               }
 
               function removeDimensionPair() {
-                join.removeCondition(index).parent().update(updateQuery);
+                updateQuery(join.removeCondition(index).parent());
               }
 
               function updateOperator({ target: { value } }) {
-                join.setOperator(index, value).parent().update(updateQuery);
+                updateQuery(join.setOperator(index, value).parent());
               }
 
               return (
@@ -417,7 +417,7 @@ function JoinTablePicker({
       .setJoinSourceTableId(tableId)
       .setDefaultCondition()
       .setDefaultAlias();
-    newJoin.parent().update(updateQuery);
+    updateQuery(newJoin.parent());
     onSourceTableSet(newJoin);
   }
 
@@ -469,7 +469,7 @@ function JoinTypePicker({ join, color, updateQuery }) {
   const strategyOption = join.strategyOption();
 
   function onChange(strategy) {
-    join.setStrategy(strategy).parent().update(updateQuery);
+    updateQuery(join.setStrategy(strategy).parent());
   }
 
   return (
@@ -669,28 +669,29 @@ const JoinFieldsPicker = ({ join, updateQuery, ...props }) => {
   const selected = new Set(selectedDimensions.map(d => d.key()));
 
   function onSelectAll() {
-    join.setFields("all").parent().update(updateQuery);
+    updateQuery(join.setFields("all").parent());
   }
 
   function onSelectNone() {
-    join.setFields("none").parent().update(updateQuery);
+    updateQuery(join.setFields("none").parent());
   }
 
   function onToggleDimension(dimension) {
-    join
-      .setFields(
-        dimensions
-          .filter(d => {
-            if (d === dimension) {
-              return !selected.has(d.key());
-            } else {
-              return selected.has(d.key());
-            }
-          })
-          .map(d => d.mbql()),
-      )
-      .parent()
-      .update(updateQuery);
+    updateQuery(
+      join
+        .setFields(
+          dimensions
+            .filter(d => {
+              if (d === dimension) {
+                return !selected.has(d.key());
+              } else {
+                return selected.has(d.key());
+              }
+            })
+            .map(d => d.mbql()),
+        )
+        .parent(),
+    );
   }
 
   return (
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/LimitStep.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/LimitStep.jsx
index 386e48520d1..4753a86a871 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/LimitStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/LimitStep.jsx
@@ -24,7 +24,7 @@ export default function LimitStep({
         onChange={e => {
           const limit = parseInt(e.target.value, 0);
           if (limit >= 1) {
-            query.updateLimit(limit).update(updateQuery);
+            updateQuery(query.updateLimit(limit));
           }
         }}
       />
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/SortStep.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/SortStep.jsx
index 4fc7115a659..87a55fa8d2c 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/SortStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/SortStep.jsx
@@ -20,9 +20,12 @@ export default function SortStep({
           className="flex align-center"
           onClick={e => {
             e.stopPropagation();
-            query
-              .updateSort(index, [sort[0] === "asc" ? "desc" : "asc", sort[1]])
-              .update(updateQuery);
+            updateQuery(
+              query.updateSort(index, [
+                sort[0] === "asc" ? "desc" : "asc",
+                sort[1],
+              ]),
+            );
           }}
         >
           <Icon
@@ -38,13 +41,13 @@ export default function SortStep({
           sort={sort}
           onChangeSort={newSort =>
             sort
-              ? query.updateSort(index, newSort).update(updateQuery)
-              : query.sort(newSort).update(updateQuery)
+              ? updateQuery(query.updateSort(index, newSort))
+              : updateQuery(query.sort(newSort))
           }
         />
       )}
       isLastOpened={isLastOpened}
-      onRemove={(sort, index) => query.removeSort(index).update(updateQuery)}
+      onRemove={(sort, index) => updateQuery(query.removeSort(index))}
     />
   );
 }
diff --git a/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx b/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx
index e14280f27d8..a6c867d898b 100644
--- a/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx
+++ b/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx
@@ -56,13 +56,17 @@ export default class NativeQueryButton extends React.Component {
     this.setState({ open: false });
   };
   handleConvert = () => {
-    this.props.question
-      .setDatasetQuery({
-        type: "native",
-        native: { query: this.getFormattedQuery() },
-        database: this.state.datasetQuery.database,
-      })
-      .update(null, { shouldUpdateUrl: true });
+    const { question, updateQuestion } = this.props;
+
+    const newQuestion = question.setDatasetQuery({
+      type: "native",
+      native: { query: this.getFormattedQuery() },
+      database: this.state.datasetQuery.database,
+    });
+
+    updateQuestion(newQuestion, {
+      shouldUpdateUrl: true,
+    });
   };
 
   getFormattedQuery() {
diff --git a/frontend/src/metabase/query_builder/components/view/QuestionDataSelector.jsx b/frontend/src/metabase/query_builder/components/view/QuestionDataSelector.jsx
index 1157fe9832a..f4afc303040 100644
--- a/frontend/src/metabase/query_builder/components/view/QuestionDataSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/view/QuestionDataSelector.jsx
@@ -3,7 +3,11 @@ import React from "react";
 
 import { DataSourceSelector } from "metabase/query_builder/components/DataSelector";
 
-export default function QuestionDataSelector({ query, triggerElement }) {
+export default function QuestionDataSelector({
+  query,
+  updateQuestion,
+  triggerElement,
+}) {
   return (
     <DataSourceSelector
       containerClassName="DataPopoverContainer"
@@ -12,7 +16,9 @@ export default function QuestionDataSelector({ query, triggerElement }) {
       selectedDatabaseId={query.databaseId()}
       selectedTableId={query.tableId()}
       setSourceTableFn={tableId =>
-        query.setTableId(tableId).setDefaultQuery().update(null, { run: true })
+        updateQuestion(query.setTableId(tableId).setDefaultQuery().question(), {
+          run: true,
+        })
       }
       triggerElement={triggerElement}
       isOpen
diff --git a/frontend/src/metabase/query_builder/components/view/QuestionFilters.jsx b/frontend/src/metabase/query_builder/components/view/QuestionFilters.jsx
index bcf04213ff7..4dca876c189 100644
--- a/frontend/src/metabase/query_builder/components/view/QuestionFilters.jsx
+++ b/frontend/src/metabase/query_builder/components/view/QuestionFilters.jsx
@@ -25,12 +25,14 @@ export default function QuestionFilters({
   expanded,
   onExpand,
   onCollapse,
+  onQueryChange,
 }) {
   const query = question.query();
   const filters = query.topLevelFilters();
   if (filters.length === 0) {
     return null;
   }
+
   return (
     <div className={className}>
       <div className="flex flex-wrap align-center mbn1 mrn1">
@@ -56,9 +58,7 @@ export default function QuestionFilters({
               key={index}
               triggerElement={
                 <FilterPill
-                  onRemove={() =>
-                    filter.remove().rootQuery().update(null, { run: true })
-                  }
+                  onRemove={() => onQueryChange(filter.remove().rootQuery())}
                 >
                   {filter.displayName()}
                 </FilterPill>
@@ -71,7 +71,7 @@ export default function QuestionFilters({
                 query={query}
                 filter={filter}
                 onChangeFilter={newFilter =>
-                  newFilter.replace().rootQuery().update(null, { run: true })
+                  onQueryChange(newFilter.replace().rootQuery())
                 }
                 className="scroll-y"
               />
@@ -88,6 +88,7 @@ export function FilterHeaderToggle({
   onExpand,
   expanded,
   onCollapse,
+  onQueryChange,
 }) {
   const query = question.query();
   const filters = query.topLevelFilters();
@@ -112,7 +113,7 @@ export function FilterHeaderToggle({
   );
 }
 
-export function FilterHeader({ question, expanded }) {
+export function FilterHeader({ question, expanded, onQueryChange }) {
   const query = question.query();
   const filters = query.topLevelFilters();
   if (filters.length === 0 || !expanded) {
@@ -126,9 +127,7 @@ export function FilterHeader({ question, expanded }) {
             key={index}
             triggerElement={
               <FilterPill
-                onRemove={() =>
-                  filter.remove().rootQuery().update(null, { run: true })
-                }
+                onRemove={() => onQueryChange(filter.remove().rootQuery())}
               >
                 {filter.displayName()}
               </FilterPill>
@@ -141,7 +140,7 @@ export function FilterHeader({ question, expanded }) {
               query={query}
               filter={filter}
               onChangeFilter={newFilter =>
-                newFilter.replace().rootQuery().update(null, { run: true })
+                onQueryChange(newFilter.replace().rootQuery())
               }
               className="scroll-y"
             />
diff --git a/frontend/src/metabase/query_builder/components/view/QuestionRowCount.jsx b/frontend/src/metabase/query_builder/components/view/QuestionRowCount.jsx
index fca0bd339a2..50a16e98614 100644
--- a/frontend/src/metabase/query_builder/components/view/QuestionRowCount.jsx
+++ b/frontend/src/metabase/query_builder/components/view/QuestionRowCount.jsx
@@ -16,6 +16,7 @@ const QuestionRowCount = ({
   result,
   className,
   isResultDirty,
+  onQueryChange,
   ...props
 }) => {
   const formatRowCount = count => {
@@ -71,9 +72,9 @@ const QuestionRowCount = ({
             limit={limit}
             onChangeLimit={limit => {
               if (limit > 0) {
-                query.updateLimit(limit).update();
+                onQueryChange(query.updateLimit(limit));
               } else {
-                query.clearLimit().update();
+                onQueryChange(query.clearLimit());
               }
             }}
             onClose={onClose}
diff --git a/frontend/src/metabase/query_builder/components/view/View.jsx b/frontend/src/metabase/query_builder/components/view/View.jsx
index d339f7ec596..49d8875e337 100644
--- a/frontend/src/metabase/query_builder/components/view/View.jsx
+++ b/frontend/src/metabase/query_builder/components/view/View.jsx
@@ -60,6 +60,10 @@ class View extends React.Component {
     ...DEFAULT_POPOVER_STATE,
   };
 
+  onUpdateQuery = (query, options = { run: true }) => {
+    this.props.updateQuestion(query.question(), options);
+  };
+
   handleAddSeries = e => {
     this.setState({
       ...DEFAULT_POPOVER_STATE,
@@ -77,7 +81,7 @@ class View extends React.Component {
 
   handleRemoveSeries = (e, index) => {
     const { query } = this.props;
-    query.removeAggregation(index).update(null, { run: true });
+    this.onUpdateQuery(query.removeAggregation(index));
   };
 
   handleEditBreakout = (e, index) => {
@@ -98,11 +102,11 @@ class View extends React.Component {
     const { query } = this.props;
     const { aggregationIndex } = this.state;
     if (aggregationIndex != null) {
-      query
-        .updateAggregation(aggregationIndex, aggregation)
-        .update(null, { run: true });
+      this.onUpdateQuery(
+        query.updateAggregation(aggregationIndex, aggregation),
+      );
     } else {
-      query.aggregate(aggregation).update(null, { run: true });
+      this.onUpdateQuery(query.aggregate(aggregation));
     }
     this.handleClosePopover();
   };
@@ -111,9 +115,9 @@ class View extends React.Component {
     const { query } = this.props;
     const { breakoutIndex } = this.state;
     if (breakoutIndex != null) {
-      query.updateBreakout(breakoutIndex, breakout).update(null, { run: true });
+      this.onUpdateQuery(query.updateBreakout(breakoutIndex, breakout));
     } else {
-      query.breakout(breakout).update(null, { run: true });
+      this.onUpdateQuery(query.breakout(breakout));
     }
     this.handleClosePopover();
   };
@@ -148,6 +152,7 @@ class View extends React.Component {
       isShowingTimelineSidebar,
       isShowingQuestionInfoSidebar,
       runQuestionQuery,
+      updateQuestion,
       visibleTimelineIds,
       selectedTimelineEventIds,
       xDomain,
@@ -170,6 +175,7 @@ class View extends React.Component {
           onClose={onCloseSummary}
           isResultDirty={isResultDirty}
           runQuestionQuery={runQuestionQuery}
+          updateQuestion={updateQuestion}
         />
       );
     }
@@ -428,6 +434,7 @@ class View extends React.Component {
       onConfirmToast,
       isShowingToaster,
       isHeaderVisible,
+      updateQuestion,
     } = this.props;
 
     // if we don't have a card at all or no databases then we are initializing, so keep it simple
@@ -441,7 +448,13 @@ class View extends React.Component {
       isStructured && !query.sourceTableId() && !query.sourceQuery();
 
     if (isNewQuestion && queryBuilderMode === "view") {
-      return <NewQuestionView query={query} className="full-height" />;
+      return (
+        <NewQuestionView
+          query={query}
+          updateQuestion={updateQuestion}
+          className="full-height"
+        />
+      );
     }
 
     if (card.dataset && queryBuilderMode === "dataset") {
diff --git a/frontend/src/metabase/query_builder/components/view/View/NewQuestionView/NewQuestionView.tsx b/frontend/src/metabase/query_builder/components/view/View/NewQuestionView/NewQuestionView.tsx
index 14e56f986a6..f3badfa472f 100644
--- a/frontend/src/metabase/query_builder/components/view/View/NewQuestionView/NewQuestionView.tsx
+++ b/frontend/src/metabase/query_builder/components/view/View/NewQuestionView/NewQuestionView.tsx
@@ -2,20 +2,23 @@ import React from "react";
 import { t } from "ttag";
 
 import Subhead from "metabase/components/type/Subhead";
+import type { updateQuestion } from "metabase/query_builder/actions";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 import QuestionDataSelector from "../../QuestionDataSelector";
 
 type Props = {
   query: StructuredQuery;
+  updateQuestion: typeof updateQuestion;
 };
 
-function NewQuestionView({ query }: Props) {
+function NewQuestionView({ query, updateQuestion }: Props) {
   return (
     <div className="full-height">
       <div className="p4 mx2">
         <QuestionDataSelector
           query={query}
+          updateQuestion={updateQuestion}
           triggerElement={
             <Subhead className="mb2">{t`Pick your data`}</Subhead>
           }
diff --git a/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx b/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx
index 4c0c276171b..8bb2455247d 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx
+++ b/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx
@@ -1,5 +1,5 @@
 /* eslint-disable react/prop-types */
-import React from "react";
+import React, { useCallback } from "react";
 
 import { t } from "ttag";
 import cx from "classnames";
@@ -51,7 +51,16 @@ const ViewFooter = ({
   isShowingTimelineSidebar,
   onOpenTimelines,
   onCloseTimelines,
+  updateQuestion,
 }) => {
+  const onQueryChange = useCallback(
+    query => {
+      const newQuestion = query.question();
+      updateQuestion(newQuestion, { run: true });
+    },
+    [updateQuestion],
+  );
+
   if (!result) {
     return null;
   }
@@ -117,6 +126,7 @@ const ViewFooter = ({
               question={question}
               isResultDirty={isResultDirty}
               result={result}
+              onQueryChange={onQueryChange}
             />
           ),
           QuestionLastUpdated.shouldRender({ result }) && (
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx b/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
index 2dbf8897689..84ae8f94b1a 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
@@ -69,6 +69,7 @@ const viewTitleHeaderPropTypes = {
 
   runQuestionQuery: PropTypes.func,
   cancelQuery: PropTypes.func,
+  updateQuestion: PropTypes.func,
 
   onOpenModal: PropTypes.func,
   onEditSummary: PropTypes.func,
@@ -80,7 +81,7 @@ const viewTitleHeaderPropTypes = {
 };
 
 export function ViewTitleHeader(props) {
-  const { question, className, style, isNavBarOpen } = props;
+  const { question, className, style, isNavBarOpen, updateQuestion } = props;
 
   const [
     areFiltersExpanded,
@@ -110,6 +111,13 @@ export function ViewTitleHeader(props) {
   const isSummarized =
     isStructured && question.query().topLevelQuery().hasAggregations();
 
+  const onQueryChange = useCallback(
+    newQuery => {
+      updateQuestion(newQuery.question(), { run: true });
+    },
+    [updateQuestion],
+  );
+
   return (
     <>
       <ViewHeaderContainer
@@ -136,6 +144,7 @@ export function ViewTitleHeader(props) {
           areFiltersExpanded={areFiltersExpanded}
           onExpandFilters={expandFilters}
           onCollapseFilters={collapseFilters}
+          onQueryChange={onQueryChange}
         />
       </ViewHeaderContainer>
       {QuestionFilters.shouldRender(props) && (
@@ -143,6 +152,7 @@ export function ViewTitleHeader(props) {
           {...props}
           expanded={areFiltersExpanded}
           question={question}
+          onQueryChange={onQueryChange}
         />
       )}
     </>
@@ -322,6 +332,7 @@ ViewTitleHeaderRightSide.propTypes = {
   isResultDirty: PropTypes.bool,
   isActionListVisible: PropTypes.bool,
   runQuestionQuery: PropTypes.func,
+  updateQuestion: PropTypes.func.isRequired,
   cancelQuery: PropTypes.func,
   onOpenModal: PropTypes.func,
   onEditSummary: PropTypes.func,
@@ -339,6 +350,7 @@ ViewTitleHeaderRightSide.propTypes = {
   onCloseQuestionInfo: PropTypes.func,
   isShowingQuestionInfoSidebar: PropTypes.bool,
   onModelPersistenceChange: PropTypes.bool,
+  onQueryChange: PropTypes.func,
 };
 
 function ViewTitleHeaderRightSide(props) {
@@ -359,6 +371,7 @@ function ViewTitleHeaderRightSide(props) {
     isResultDirty,
     isActionListVisible,
     runQuestionQuery,
+    updateQuestion,
     cancelQuery,
     onOpenModal,
     onEditSummary,
@@ -374,6 +387,7 @@ function ViewTitleHeaderRightSide(props) {
     onCloseQuestionInfo,
     onOpenQuestionInfo,
     onModelPersistenceChange,
+    onQueryChange,
   } = props;
   const isShowingNotebook = queryBuilderMode === "notebook";
   const query = question.query();
@@ -416,6 +430,7 @@ function ViewTitleHeaderRightSide(props) {
           expanded={areFiltersExpanded}
           onExpand={onExpandFilters}
           onCollapse={onCollapseFilters}
+          onQueryChange={onQueryChange}
         />
       )}
       {QuestionFilterWidget.shouldRender(props) && (
@@ -453,6 +468,7 @@ function ViewTitleHeaderRightSide(props) {
           <NativeQueryButton
             size={16}
             question={question}
+            updateQuestion={updateQuestion}
             data-metabase-event="Notebook Mode; Convert to SQL Click"
           />
         </ViewHeaderIconButtonContainer>
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/ChartTypeSidebar.jsx b/frontend/src/metabase/query_builder/components/view/sidebars/ChartTypeSidebar.jsx
index 9f36750575f..bdec6daee64 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/ChartTypeSidebar.jsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/ChartTypeSidebar.jsx
@@ -25,6 +25,7 @@ const ChartTypeSidebar = ({
   result,
   onOpenChartSettings,
   onCloseChartType,
+  updateQuestion,
   isShowingChartTypeSidebar,
   setUIControls,
   ...props
@@ -64,13 +65,14 @@ const ChartTypeSidebar = ({
                     visualization.isSensible(result.data, props.query)
                   }
                   onClick={() => {
-                    question
+                    const newQuestion = question
                       .setDisplay(type)
-                      .lockDisplay(true) // prevent viz auto-selection
-                      .update(null, {
-                        reload: false,
-                        shouldUpdateUrl: question.query().isEditable(),
-                      });
+                      .lockDisplay(true); // prevent viz auto-selection
+
+                    updateQuestion(newQuestion, {
+                      reload: false,
+                      shouldUpdateUrl: question.query().isEditable(),
+                    });
                     onOpenChartSettings({ section: t`Data` });
                     setUIControls({ isShowingRawTable: false });
                   }}
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AddAggregationButton/AddAggregationButton.jsx b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AddAggregationButton/AddAggregationButton.jsx
index 8b10752865b..159eb3f10fb 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AddAggregationButton/AddAggregationButton.jsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AddAggregationButton/AddAggregationButton.jsx
@@ -7,17 +7,21 @@ import AggregationPopover from "metabase/query_builder/components/AggregationPop
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
 import Icon from "metabase/components/Icon";
 
-import { updateAndRunQuery } from "../utils";
 import { AddAggregationButtonRoot } from "./AddAggregationButton.styled";
 
 const propTypes = {
   query: PropTypes.object,
   shouldShowLabel: PropTypes.boolean,
+  updateAndRunQuery: PropTypes.func.isRequired,
 };
 
 const LABEL = t`Add a metric`;
 
-export const AddAggregationButton = ({ query, shouldShowLabel = false }) => {
+export const AddAggregationButton = ({
+  query,
+  shouldShowLabel = false,
+  updateAndRunQuery,
+}) => {
   return (
     <PopoverWithTrigger
       triggerElement={
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AggregationItem/AggregationItem.jsx b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AggregationItem/AggregationItem.jsx
index 34442cef3a4..5930c004f06 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AggregationItem/AggregationItem.jsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AggregationItem/AggregationItem.jsx
@@ -6,7 +6,6 @@ import AggregationPopover from "metabase/query_builder/components/AggregationPop
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
 import Icon from "metabase/components/Icon";
 
-import { updateAndRunQuery } from "../utils";
 import { AggregationItemRoot } from "./AggregationItem.styled";
 
 const propTypes = {
@@ -15,9 +14,16 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   query: PropTypes.object,
   onRemove: PropTypes.func,
+  updateAndRunQuery: PropTypes.func.isRequired,
 };
 
-export const AggregationItem = ({ aggregation, index, query, onRemove }) => {
+export const AggregationItem = ({
+  aggregation,
+  index,
+  query,
+  onRemove,
+  updateAndRunQuery,
+}) => {
   return (
     <PopoverWithTrigger
       triggerClasses="flex-full"
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/SummarizeSidebar.jsx b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/SummarizeSidebar.jsx
index eafc98408e2..10a17ba27d8 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/SummarizeSidebar.jsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/SummarizeSidebar.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useCallback } from "react";
 import PropTypes from "prop-types";
 import { t } from "ttag";
 import cx from "classnames";
@@ -6,7 +6,6 @@ import cx from "classnames";
 import { color } from "metabase/lib/colors";
 import SidebarContent from "metabase/query_builder/components/SidebarContent";
 
-import { updateAndRunQuery } from "./utils";
 import { AddAggregationButton } from "./AddAggregationButton";
 import { AggregationItem } from "./AggregationItem";
 import { DimensionList } from "./DimensionList";
@@ -20,6 +19,7 @@ const propTypes = {
   question: PropTypes.object,
   isResultDirty: PropTypes.bool,
   runQuestionQuery: PropTypes.func.isRequired,
+  updateQuestion: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,
   className: PropTypes.string,
 };
@@ -35,6 +35,7 @@ const SummarizeSidebar = ({
   question,
   isResultDirty,
   runQuestionQuery,
+  updateQuestion,
   onClose,
   className,
 }) => {
@@ -45,6 +46,15 @@ const SummarizeSidebar = ({
     getQuery(question, isDefaultAggregationRemoved),
   );
 
+  const updateAndRunQuery = useCallback(
+    query => {
+      updateQuestion(query.question().setDefaultDisplay(), {
+        run: true,
+      });
+    },
+    [updateQuestion],
+  );
+
   useEffect(() => {
     const nextQuery = getQuery(question, isDefaultAggregationRemoved);
     setQuery(nextQuery);
@@ -96,7 +106,7 @@ const SummarizeSidebar = ({
           runQuestionQuery();
         }
         if (hasDefaultAggregation) {
-          query.update(null, { run: true });
+          updateQuestion(query.question(), { run: true });
         }
         onClose();
       }}
@@ -110,11 +120,13 @@ const SummarizeSidebar = ({
             index={index}
             query={query}
             onRemove={handleAggregationRemove}
+            updateAndRunQuery={updateAndRunQuery}
           />
         ))}
         <AddAggregationButton
           query={query}
           shouldShowLabel={!hasAggregations}
+          updateAndRunQuery={updateAndRunQuery}
         />
       </AggregationsContainer>
 
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/utils.js b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/utils.js
deleted file mode 100644
index 39c56fdd3b0..00000000000
--- a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/utils.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// set the display automatically then run
-export function updateAndRunQuery(query) {
-  query.question().setDefaultDisplay().update(null, { run: true });
-}
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
index 1e8b10ea526..649cd3d6e2b 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
@@ -206,7 +206,6 @@ function QueryBuilder(props) {
     initializeQB,
     apiCreateQuestion,
     apiUpdateQuestion,
-    updateQuestion,
     updateUrl,
     locationChanged,
     onChangeLocation,
@@ -357,12 +356,6 @@ function QueryBuilder(props) {
     }
   }, [location, params, previousLocation, locationChanged]);
 
-  useEffect(() => {
-    if (question) {
-      question._update = updateQuestion;
-    }
-  });
-
   const [isShowingToaster, setIsShowingToaster] = useState(false);
 
   const { isRunning } = uiControls;
-- 
GitLab