diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx b/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx
deleted file mode 100644
index caf4451c3bd7937a90df14e26ffc6c3d370cef44..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx
+++ /dev/null
@@ -1,268 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from "react";
-
-import { t } from "ttag";
-import _ from "underscore";
-
-import styled from "@emotion/styled";
-
-import { color as c, lighten, darken, alpha } from "metabase/lib/colors";
-
-import Tooltip from "metabase/core/components/Tooltip";
-import Icon from "metabase/components/Icon";
-import Button from "metabase/core/components/Button";
-import ExpandingContent from "metabase/components/ExpandingContent";
-
-import NotebookStepPreview from "./NotebookStepPreview";
-
-import DataStep from "./steps/DataStep";
-import JoinStep from "./steps/JoinStep";
-import ExpressionStep from "./steps/ExpressionStep";
-import FilterStep from "./steps/FilterStep";
-import AggregateStep from "./steps/AggregateStep";
-import BreakoutStep from "./steps/BreakoutStep";
-import SummarizeStep from "./steps/SummarizeStep";
-import SortStep from "./steps/SortStep";
-import LimitStep from "./steps/LimitStep";
-import {
-  StepActionsContainer,
-  StepBody,
-  StepContent,
-  StepHeader,
-  StepButtonContainer,
-  StepRoot,
-} from "./NotebookStep.styled";
-
-// TODO
-const STEP_UI = {
-  data: {
-    title: t`Data`,
-    component: DataStep,
-    getColor: () => c("brand"),
-  },
-  join: {
-    title: t`Join data`,
-    icon: "join_left_outer",
-    component: JoinStep,
-    priority: 1,
-    getColor: () => c("brand"),
-  },
-  expression: {
-    title: t`Custom column`,
-    icon: "add_data",
-    component: ExpressionStep,
-    transparent: true,
-    getColor: () => c("bg-dark"),
-  },
-  filter: {
-    title: t`Filter`,
-    icon: "filter",
-    component: FilterStep,
-    priority: 10,
-    getColor: () => c("filter"),
-  },
-  summarize: {
-    title: t`Summarize`,
-    icon: "sum",
-    component: SummarizeStep,
-    priority: 5,
-    getColor: () => c("summarize"),
-  },
-  aggregate: {
-    title: t`Aggregate`,
-    icon: "sum",
-    component: AggregateStep,
-    priority: 5,
-    getColor: () => c("summarize"),
-  },
-  breakout: {
-    title: t`Breakout`,
-    icon: "segment",
-    component: BreakoutStep,
-    priority: 1,
-    getColor: () => c("accent4"),
-  },
-  sort: {
-    title: t`Sort`,
-    icon: "smartscalar",
-    component: SortStep,
-    compact: true,
-    transparent: true,
-    getColor: () => c("bg-dark"),
-  },
-  limit: {
-    title: t`Row limit`,
-    icon: "list",
-    component: LimitStep,
-    compact: true,
-    transparent: true,
-    getColor: () => c("bg-dark"),
-  },
-};
-
-function getTestId(step) {
-  const { type, stageIndex, itemIndex } = step;
-  return `step-${type}-${stageIndex || 0}-${itemIndex || 0}`;
-}
-
-export default class NotebookStep extends React.Component {
-  state = {
-    showPreview: false,
-  };
-
-  render() {
-    const {
-      step,
-      openStep,
-      isLastStep,
-      isLastOpened,
-      updateQuery,
-      reportTimezone,
-      sourceQuestion,
-    } = this.props;
-    const { showPreview } = this.state;
-
-    const {
-      title,
-      getColor,
-      component: NotebookStepComponent,
-    } = STEP_UI[step.type] || {};
-
-    const color = getColor();
-    const canPreview = step.previewQuery && step.previewQuery.isValid();
-    const showPreviewButton = !showPreview && canPreview;
-
-    const largeActionButtons =
-      isLastStep &&
-      _.any(step.actions, action => !STEP_UI[action.type].compact);
-
-    const actions = [];
-    actions.push(
-      ...step.actions.map(action => {
-        const stepUi = STEP_UI[action.type];
-
-        return {
-          priority: stepUi.priority,
-          button: (
-            <ActionButton
-              mr={isLastStep ? 2 : 1}
-              mt={isLastStep ? 2 : null}
-              color={stepUi.getColor()}
-              large={largeActionButtons}
-              {...stepUi}
-              key={`actionButton_${stepUi.title}`}
-              onClick={() => action.action({ query: step.query, openStep })}
-            />
-          ),
-        };
-      }),
-    );
-
-    actions.sort((a, b) => (b.priority || 0) - (a.priority || 0));
-    const actionButtons = actions.map(action => action.button);
-
-    return (
-      <ExpandingContent isInitiallyOpen={!isLastOpened} isOpen>
-        <StepRoot
-          className="hover-parent hover--visibility"
-          data-testid={getTestId(step)}
-        >
-          <StepHeader color={color}>
-            {title}
-            <Icon
-              name="close"
-              className="ml-auto cursor-pointer text-light text-medium-hover hover-child"
-              tooltip={t`Remove`}
-              onClick={() => updateQuery(step.revert(step.query))}
-              data-testid="remove-step"
-            />
-          </StepHeader>
-
-          {NotebookStepComponent && (
-            <StepBody>
-              <StepContent>
-                <NotebookStepComponent
-                  color={color}
-                  step={step}
-                  query={step.query}
-                  sourceQuestion={sourceQuestion}
-                  updateQuery={updateQuery}
-                  isLastOpened={isLastOpened}
-                  reportTimezone={reportTimezone}
-                />
-              </StepContent>
-              <StepButtonContainer>
-                <ActionButton
-                  ml={[1, 2]}
-                  className={
-                    !showPreviewButton ? "hidden disabled" : "text-brand-hover"
-                  }
-                  icon="play"
-                  title={t`Preview`}
-                  color={c("text-light")}
-                  transparent
-                  onClick={() => this.setState({ showPreview: true })}
-                />
-              </StepButtonContainer>
-            </StepBody>
-          )}
-
-          {showPreview && canPreview && (
-            <NotebookStepPreview
-              step={step}
-              onClose={() => this.setState({ showPreview: false })}
-            />
-          )}
-
-          {actionButtons.length > 0 && (
-            <StepActionsContainer data-testid="action-buttons">
-              {actionButtons}
-            </StepActionsContainer>
-          )}
-        </StepRoot>
-      </ExpandingContent>
-    );
-  }
-}
-
-const ColorButton = styled(Button)`
-  border: none;
-  color: ${({ color }) => color};
-  background-color: ${({ color, transparent }) =>
-    transparent ? null : alpha(color, 0.2)};
-  &:hover {
-    color: ${({ color }) => darken(color, 0.115)};
-    background-color: ${({ color, transparent }) =>
-      transparent ? lighten(color, 0.5) : alpha(color, 0.35)};
-  }
-  transition: background 300ms;
-`;
-
-const ActionButton = ({
-  icon,
-  title,
-  color,
-  transparent,
-  large,
-  onClick,
-  ...props
-}) => {
-  const label = large ? title : null;
-  const button = (
-    <ColorButton
-      icon={icon}
-      small={!large}
-      color={color}
-      transparent={transparent}
-      iconVertical={large}
-      iconSize={large ? 18 : 14}
-      onClick={onClick}
-      aria-label={label}
-      {...props}
-    >
-      {label}
-    </ColorButton>
-  );
-
-  return large ? button : <Tooltip tooltip={title}>{button}</Tooltip>;
-};
diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookStep/ActionButton.tsx b/frontend/src/metabase/query_builder/components/notebook/NotebookStep/ActionButton.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8d7307d71fd54f0fa45b596b73a66a04d54634e1
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/notebook/NotebookStep/ActionButton.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import Tooltip from "metabase/core/components/Tooltip";
+import { ColorButton } from "./NotebookStep.styled";
+
+interface ActionButtonProps {
+  className?: string;
+
+  icon?: string;
+  title: string;
+  color: string;
+  transparent?: boolean;
+  large?: boolean;
+  onClick: () => void;
+
+  // styled-system props
+  mt?: number | number[];
+  mr?: number | number[];
+  ml?: number | number[];
+}
+
+function ActionButton({
+  icon,
+  title,
+  color,
+  transparent,
+  large,
+  onClick,
+  ...props
+}: ActionButtonProps) {
+  const label = large ? title : undefined;
+
+  const button = (
+    <ColorButton
+      icon={icon}
+      small={!large}
+      color={color}
+      transparent={transparent}
+      iconVertical={large}
+      iconSize={large ? 18 : 14}
+      aria-label={label}
+      onClick={onClick}
+      {...props}
+    >
+      {label}
+    </ColorButton>
+  );
+
+  return large ? button : <Tooltip tooltip={title}>{button}</Tooltip>;
+}
+
+export default ActionButton;
diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookStep.styled.tsx b/frontend/src/metabase/query_builder/components/notebook/NotebookStep/NotebookStep.styled.tsx
similarity index 61%
rename from frontend/src/metabase/query_builder/components/notebook/NotebookStep.styled.tsx
rename to frontend/src/metabase/query_builder/components/notebook/NotebookStep/NotebookStep.styled.tsx
index baa9258251389f4eee49fbf47df93c444095119f..3b4cf72909910a1044fdebd1e6e958cc0a257314 100644
--- a/frontend/src/metabase/query_builder/components/notebook/NotebookStep.styled.tsx
+++ b/frontend/src/metabase/query_builder/components/notebook/NotebookStep/NotebookStep.styled.tsx
@@ -1,4 +1,6 @@
 import styled from "@emotion/styled";
+import Button from "metabase/core/components/Button";
+import { alpha, darken, lighten } from "metabase/lib/colors";
 import { breakpointMinSmall } from "metabase/styled-components/theme";
 
 const getPercentage = (number: number): string => {
@@ -46,3 +48,21 @@ export const StepButtonContainer = styled.div`
 export const StepActionsContainer = styled.div`
   margin-top: 0.5rem;
 `;
+
+interface ColorButtonProps {
+  color: string;
+  transparent?: boolean;
+}
+
+export const ColorButton = styled(Button)<ColorButtonProps>`
+  border: none;
+  color: ${({ color }) => color};
+  background-color: ${({ color, transparent }) =>
+    transparent ? null : alpha(color, 0.2)};
+  &:hover {
+    color: ${({ color }) => darken(color, 0.115)};
+    background-color: ${({ color, transparent }) =>
+      transparent ? lighten(color, 0.5) : alpha(color, 0.35)};
+  }
+  transition: background 300ms;
+`;
diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookStep/NotebookStep.tsx b/frontend/src/metabase/query_builder/components/notebook/NotebookStep/NotebookStep.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2c53c1f2ed9d7b612c081017aa1e710e4ddbe6bb
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/notebook/NotebookStep/NotebookStep.tsx
@@ -0,0 +1,168 @@
+import React, { useCallback, useMemo } from "react";
+import { t } from "ttag";
+
+import { color as c } from "metabase/lib/colors";
+import { useToggle } from "metabase/hooks/use-toggle";
+
+import Icon from "metabase/components/Icon";
+import ExpandingContent from "metabase/components/ExpandingContent";
+
+import type Question from "metabase-lib/Question";
+import type StructuredQuery from "metabase-lib/queries/StructuredQuery";
+
+import {
+  NotebookStep as INotebookStep,
+  NotebookStepAction,
+} from "../lib/steps.types";
+import NotebookStepPreview from "../NotebookStepPreview";
+
+import { STEP_UI } from "./steps";
+import ActionButton from "./ActionButton";
+import {
+  StepActionsContainer,
+  StepBody,
+  StepContent,
+  StepHeader,
+  StepButtonContainer,
+  StepRoot,
+} from "./NotebookStep.styled";
+
+function hasLargeButton(action: NotebookStepAction) {
+  return !STEP_UI[action.type].compact;
+}
+
+function getTestId(step: INotebookStep) {
+  const { type, stageIndex, itemIndex } = step;
+  return `step-${type}-${stageIndex || 0}-${itemIndex || 0}`;
+}
+
+interface NotebookStepProps {
+  step: INotebookStep;
+  sourceQuestion?: Question;
+  isLastStep: boolean;
+  isLastOpened: boolean;
+  reportTimezone?: string;
+  openStep: (id: string) => void;
+  updateQuery: (query: StructuredQuery) => Promise<void>;
+}
+
+function NotebookStep({
+  step,
+  sourceQuestion,
+  isLastStep,
+  isLastOpened,
+  reportTimezone,
+  openStep,
+  updateQuery,
+}: NotebookStepProps) {
+  const [isPreviewOpen, { turnOn: openPreview, turnOff: closePreview }] =
+    useToggle(false);
+
+  const actionButtons = useMemo(() => {
+    const actions = [];
+    const hasLargeActionButtons =
+      isLastStep && step.actions.some(hasLargeButton);
+
+    actions.push(
+      ...step.actions.map(action => {
+        const stepUi = STEP_UI[action.type];
+        return {
+          priority: stepUi.priority,
+          button: (
+            <ActionButton
+              key={`actionButton_${stepUi.title}`}
+              mr={isLastStep ? 2 : 1}
+              mt={isLastStep ? 2 : undefined}
+              color={stepUi.getColor()}
+              large={hasLargeActionButtons}
+              {...stepUi}
+              onClick={() => action.action({ query: step.query, openStep })}
+            />
+          ),
+        };
+      }),
+    );
+
+    actions.sort((a, b) => (b.priority || 0) - (a.priority || 0));
+
+    return actions.map(action => action.button);
+  }, [step.query, step.actions, isLastStep, openStep]);
+
+  const handleClickRevert = useCallback(() => {
+    const reverted = step.revert?.(step.query);
+    if (reverted) {
+      updateQuery(reverted);
+    }
+  }, [step, updateQuery]);
+
+  const {
+    title,
+    getColor,
+    component: NotebookStepComponent,
+  } = STEP_UI[step.type] || {};
+
+  const color = getColor();
+  const canPreview = step?.previewQuery?.isValid?.();
+  const hasPreviewButton = !isPreviewOpen && canPreview;
+
+  return (
+    <ExpandingContent isInitiallyOpen={!isLastOpened} isOpen>
+      <StepRoot
+        className="hover-parent hover--visibility"
+        data-testid={getTestId(step)}
+      >
+        <StepHeader color={color}>
+          {title}
+          <Icon
+            name="close"
+            className="ml-auto cursor-pointer text-light text-medium-hover hover-child"
+            tooltip={t`Remove`}
+            onClick={handleClickRevert}
+            data-testid="remove-step"
+          />
+        </StepHeader>
+
+        {NotebookStepComponent && (
+          <StepBody>
+            <StepContent>
+              <NotebookStepComponent
+                color={color}
+                step={step}
+                query={step.query}
+                sourceQuestion={sourceQuestion}
+                updateQuery={updateQuery}
+                isLastOpened={isLastOpened}
+                reportTimezone={reportTimezone}
+              />
+            </StepContent>
+            <StepButtonContainer>
+              <ActionButton
+                ml={[1, 2]}
+                className={
+                  !hasPreviewButton ? "hidden disabled" : "text-brand-hover"
+                }
+                icon="play"
+                title={t`Preview`}
+                color={c("text-light")}
+                transparent
+                onClick={openPreview}
+              />
+            </StepButtonContainer>
+          </StepBody>
+        )}
+
+        {canPreview && isPreviewOpen && (
+          <NotebookStepPreview step={step} onClose={closePreview} />
+        )}
+
+        {actionButtons.length > 0 && (
+          <StepActionsContainer data-testid="action-buttons">
+            {actionButtons}
+          </StepActionsContainer>
+        )}
+      </StepRoot>
+    </ExpandingContent>
+  );
+}
+
+export default NotebookStep;
diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookStep/index.ts b/frontend/src/metabase/query_builder/components/notebook/NotebookStep/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..db7282c00b0ce92ba04068b3fec87a609fa99a1e
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/notebook/NotebookStep/index.ts
@@ -0,0 +1 @@
+export { default } from "./NotebookStep";
diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookStep/steps.ts b/frontend/src/metabase/query_builder/components/notebook/NotebookStep/steps.ts
new file mode 100644
index 0000000000000000000000000000000000000000..63bc38e8b3d9707f0ba4e2f9e9925391878c480f
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/notebook/NotebookStep/steps.ts
@@ -0,0 +1,92 @@
+import React from "react";
+import { t } from "ttag";
+
+import { color } from "metabase/lib/colors";
+
+import DataStep from "../steps/DataStep";
+import JoinStep from "../steps/JoinStep";
+import ExpressionStep from "../steps/ExpressionStep";
+import FilterStep from "../steps/FilterStep";
+import AggregateStep from "../steps/AggregateStep";
+import BreakoutStep from "../steps/BreakoutStep";
+import SummarizeStep from "../steps/SummarizeStep";
+import SortStep from "../steps/SortStep";
+import LimitStep from "../steps/LimitStep";
+
+export type StepUIItem = {
+  title: string;
+  icon?: string;
+  priority?: number;
+  transparent?: boolean;
+  compact?: boolean;
+  getColor: () => string;
+
+  // Remove any once all step components are typed
+  component: React.ComponentType<any>;
+};
+
+export const STEP_UI: Record<string, StepUIItem> = {
+  data: {
+    title: t`Data`,
+    component: DataStep,
+    getColor: () => color("brand"),
+  },
+  join: {
+    title: t`Join data`,
+    icon: "join_left_outer",
+    component: JoinStep,
+    priority: 1,
+    getColor: () => color("brand"),
+  },
+  expression: {
+    title: t`Custom column`,
+    icon: "add_data",
+    component: ExpressionStep,
+    transparent: true,
+    getColor: () => color("bg-dark"),
+  },
+  filter: {
+    title: t`Filter`,
+    icon: "filter",
+    component: FilterStep,
+    priority: 10,
+    getColor: () => color("filter"),
+  },
+  summarize: {
+    title: t`Summarize`,
+    icon: "sum",
+    component: SummarizeStep,
+    priority: 5,
+    getColor: () => color("summarize"),
+  },
+  aggregate: {
+    title: t`Aggregate`,
+    icon: "sum",
+    component: AggregateStep,
+    priority: 5,
+    getColor: () => color("summarize"),
+  },
+  breakout: {
+    title: t`Breakout`,
+    icon: "segment",
+    component: BreakoutStep,
+    priority: 1,
+    getColor: () => color("accent4"),
+  },
+  sort: {
+    title: t`Sort`,
+    icon: "smartscalar",
+    component: SortStep,
+    compact: true,
+    transparent: true,
+    getColor: () => color("bg-dark"),
+  },
+  limit: {
+    title: t`Row limit`,
+    icon: "list",
+    component: LimitStep,
+    compact: true,
+    transparent: true,
+    getColor: () => color("bg-dark"),
+  },
+};
diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookSteps/NotebookSteps.tsx b/frontend/src/metabase/query_builder/components/notebook/NotebookSteps/NotebookSteps.tsx
index 557295b328305cd7fdd4b09fa5007ae5ff3742c2..728b81dc3e6255aa6fd3a9d29c1595c968c68a6b 100644
--- a/frontend/src/metabase/query_builder/components/notebook/NotebookSteps/NotebookSteps.tsx
+++ b/frontend/src/metabase/query_builder/components/notebook/NotebookSteps/NotebookSteps.tsx
@@ -4,7 +4,7 @@ import type Question from "metabase-lib/Question";
 import type StructuredQuery from "metabase-lib/queries/StructuredQuery";
 
 import { getQuestionSteps } from "../lib/steps";
-import { NotebookStep as INotebookStep } from "../lib/steps.types";
+import { NotebookStep as INotebookStep, OpenSteps } from "../lib/steps.types";
 import NotebookStep from "../NotebookStep";
 import { Container } from "./NotebookSteps.styled";
 
@@ -16,8 +16,6 @@ interface NotebookStepsProps {
   updateQuestion: (question: Question) => Promise<void>;
 }
 
-type OpenSteps = { [key: string]: boolean };
-
 function getInitialOpenSteps(question: Question): OpenSteps {
   const isNew = !question.table();
   return isNew
@@ -94,7 +92,6 @@ function NotebookSteps({
             reportTimezone={reportTimezone}
             updateQuery={onChange}
             openStep={handleStepOpen}
-            closeStep={handleStepClose}
           />
         );
       })}
diff --git a/frontend/src/metabase/query_builder/components/notebook/lib/steps.ts b/frontend/src/metabase/query_builder/components/notebook/lib/steps.ts
index f5d8a08026667e15bd50a0ce89cdd45584ffd7c1..90dfb7241f87212a1d12ceb562db5c80faeb53db 100644
--- a/frontend/src/metabase/query_builder/components/notebook/lib/steps.ts
+++ b/frontend/src/metabase/query_builder/components/notebook/lib/steps.ts
@@ -3,7 +3,7 @@ import _ from "underscore";
 import type Question from "metabase-lib/Question";
 import type StructuredQuery from "metabase-lib/queries/StructuredQuery";
 
-import { NotebookStep, NotebookStepFn } from "./steps.types";
+import { NotebookStep, NotebookStepFn, OpenSteps } from "./steps.types";
 
 // This converts an MBQL query into a sequence of notebook "steps", with special logic to determine which steps are
 // allowed to be added at every other step, generating a preview query at each step, how to delete a step,
@@ -148,7 +148,7 @@ export function getQuestionSteps(question: Question, openSteps = {}) {
 export function getStageSteps(
   stageQuery: StructuredQuery,
   stageIndex: number,
-  openSteps: Record<NotebookStep["id"], boolean>,
+  openSteps: OpenSteps,
 ) {
   const getId = (step: NotebookStepDef, itemIndex: number | null) => {
     const isValidItemIndex = itemIndex != null && itemIndex > 0;
diff --git a/frontend/src/metabase/query_builder/components/notebook/lib/steps.types.ts b/frontend/src/metabase/query_builder/components/notebook/lib/steps.types.ts
index 0b5e06525e6328cecd0e6ac75e4c46246c5f02c1..41b348e4ca7e24c0225b519fea5769c1411b59bc 100644
--- a/frontend/src/metabase/query_builder/components/notebook/lib/steps.types.ts
+++ b/frontend/src/metabase/query_builder/components/notebook/lib/steps.types.ts
@@ -53,3 +53,5 @@ export interface NotebookStepUiComponentProps {
   isLastOpened: boolean;
   reportTimezone: string;
 }
+
+export type OpenSteps = Record<NotebookStep["id"], boolean>;
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 a8eda937f6296b3ffc279498583cc45bf8054299..1c638525ffe0176101ccdac7d6da4ad6ce9d52d0 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.jsx
@@ -56,8 +56,8 @@ const stepShape = {
   clean: PropTypes.func.isRequired,
   actions: PropTypes.array.isRequired,
 };
-stepShape.previous = stepShape;
-stepShape.next = stepShape;
+stepShape.previous = PropTypes.shape(stepShape);
+stepShape.next = PropTypes.shape(stepShape);
 
 const joinStepPropTypes = {
   query: PropTypes.object.isRequired,