diff --git a/dictionary.txt b/dictionary.txt
index 1a32fec5e984f4fcaff2c252cb4d0915c9167e05..dea2f73aeade74df8f21041bfd438364f952de0c 100644
--- a/dictionary.txt
+++ b/dictionary.txt
@@ -42,6 +42,7 @@ Postel
 propless
 propsify
 Pryce's
+redoable
 reduxjs
 renderify
 rightbar
diff --git a/ui/src/components/UndoRedo.tsx b/ui/src/components/UndoRedo.tsx
index 313fbcec3a6109e61c34ecc0d385f06ff909d94e..c640bb417f3f2b300ee424a40078d497ecb0cafc 100644
--- a/ui/src/components/UndoRedo.tsx
+++ b/ui/src/components/UndoRedo.tsx
@@ -1,33 +1,13 @@
-// cspell:ignore redoable
+import { useEffect } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
 import { Button } from '@radix-ui/themes';
 import { ResetIcon } from '@radix-ui/react-icons';
-import { useAppDispatch, useAppSelector } from '@/app/hooks';
-import { selectLayoutHistory } from '@/features/layout/layoutModelSlice';
-import { selectPageDataHistory } from '@/features/pageData/pageDataSlice';
-import { useHotkeys } from 'react-hotkeys-hook';
-import { useEffect } from 'react';
-import { UndoRedoActionCreators } from '@/features/ui/uiSlice';
-import { selectUndoType, selectRedoType } from '@/features/ui/uiSlice';
-import clsx from 'clsx';
 import styles from '@/components/topbar/Topbar.module.css';
+import { useUndoRedo } from '@/hooks/useUndoRedo';
 
 const UndoRedo = () => {
-  const dispatch = useAppDispatch();
-  const layoutModel = useAppSelector(selectLayoutHistory);
-  const pageData = useAppSelector(selectPageDataHistory);
-  const undoType = useAppSelector(selectUndoType);
-  const redoType = useAppSelector(selectRedoType);
-  const isUndoable = layoutModel.past.length > 1 || pageData.past.length > 1;
-  const isRedoable =
-    layoutModel.future.length > 0 || pageData.future.length > 0;
-  const dispatchUndo = () =>
-    isUndoable && undoType
-      ? dispatch(UndoRedoActionCreators.undo(undoType))
-      : null;
-  const dispatchRedo = () =>
-    isRedoable && redoType
-      ? dispatch(UndoRedoActionCreators.redo(redoType))
-      : null;
+  const { isUndoable, isRedoable, dispatchUndo, dispatchRedo } = useUndoRedo();
+
   // The useHotKeys hook listens to the parent document.
   useHotkeys('mod+z', () => dispatchUndo()); // 'mod' listens for cmd on Mac and ctrl on Windows.
   useHotkeys(['meta+shift+z', 'ctrl+y'], () => dispatchRedo()); // Mac redo is cmd+shift+z, Windows redo is ctrl+y.
@@ -55,7 +35,7 @@ const UndoRedo = () => {
         variant="ghost"
         color="gray"
         size="2"
-        className={clsx(styles.topBarButton)}
+        className={styles.topBarButton}
         onClick={() => dispatchUndo()}
         disabled={!isUndoable}
         aria-label="Undo"
@@ -66,7 +46,7 @@ const UndoRedo = () => {
         variant="ghost"
         color="gray"
         size="2"
-        className={clsx(styles.topBarButton)}
+        className={styles.topBarButton}
         onClick={() => dispatchRedo()}
         disabled={!isRedoable}
         aria-label="Redo"
diff --git a/ui/src/features/canvas/Canvas.tsx b/ui/src/features/canvas/Canvas.tsx
index 0f6137184f2f9ac5d8b83d9af28a5f1ab64fa801..914fc70d46d57f43f5ef7eb734aeb4f6c77b34cb 100644
--- a/ui/src/features/canvas/Canvas.tsx
+++ b/ui/src/features/canvas/Canvas.tsx
@@ -23,6 +23,7 @@ import { deleteNode } from '../layout/layoutModelSlice';
 import useCopyPasteComponents from '@/hooks/useCopyPasteComponents';
 import { useNavigationUtils } from '@/hooks/useNavigationUtils';
 import useXbParams from '@/hooks/useXbParams';
+import { useUndoRedo } from '@/hooks/useUndoRedo';
 
 const Canvas = () => {
   const dispatch = useAppDispatch();
@@ -44,6 +45,7 @@ const Canvas = () => {
   const middleMouseDownRef = useRef(middleMouseDown);
   const { copySelectedComponent, pasteAfterSelectedComponent } =
     useCopyPasteComponents();
+  const { isUndoable, dispatchUndo } = useUndoRedo();
 
   useHotkeys(['NumpadAdd', 'Equal'], () => dispatch(canvasViewPortZoomIn()));
   useHotkeys(['Minus', 'NumpadSubtract'], () =>
@@ -257,6 +259,8 @@ const Canvas = () => {
               <ErrorBoundary
                 title="An unexpected error has occurred while rendering preview."
                 variant="alert"
+                onReset={isUndoable ? dispatchUndo : undefined}
+                resetButtonText={isUndoable ? 'Undo last action' : undefined}
               >
                 <Preview />
               </ErrorBoundary>
diff --git a/ui/src/hooks/useUndoRedo.ts b/ui/src/hooks/useUndoRedo.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d92ab6c0b41b656fbfcc4360bcdb7b038e54a154
--- /dev/null
+++ b/ui/src/hooks/useUndoRedo.ts
@@ -0,0 +1,44 @@
+import { useAppDispatch, useAppSelector } from '@/app/hooks';
+import {
+  UndoRedoActionCreators,
+  selectUndoType,
+  selectRedoType,
+} from '@/features/ui/uiSlice';
+import { selectLayoutHistory } from '@/features/layout/layoutModelSlice';
+import { selectPageDataHistory } from '@/features/pageData/pageDataSlice';
+
+interface UndoRedoState {
+  isUndoable: boolean;
+  isRedoable: boolean;
+  dispatchUndo: () => void;
+  dispatchRedo: () => void;
+}
+
+export function useUndoRedo(): UndoRedoState {
+  const dispatch = useAppDispatch();
+  const layoutModel = useAppSelector(selectLayoutHistory);
+  const pageData = useAppSelector(selectPageDataHistory);
+  const undoType = useAppSelector(selectUndoType);
+  const redoType = useAppSelector(selectRedoType);
+
+  const isUndoable = layoutModel.past.length > 1 || pageData.past.length > 1;
+  const isRedoable =
+    layoutModel.future.length > 0 || pageData.future.length > 0;
+
+  const dispatchUndo = () =>
+    isUndoable && undoType
+      ? dispatch(UndoRedoActionCreators.undo(undoType))
+      : null;
+
+  const dispatchRedo = () =>
+    isRedoable && redoType
+      ? dispatch(UndoRedoActionCreators.redo(redoType))
+      : null;
+
+  return {
+    isUndoable,
+    isRedoable,
+    dispatchUndo,
+    dispatchRedo,
+  };
+}