Skip to content

Conversation

@colton-demetriou
Copy link
Contributor

Started locally and verified functionality still worked. This change just adds extra safety checks for potentially null states to hopefully prevent these sentries.

https://yext-engineering.sentry.io/issues/7028835198/?environment=prod&query=is%3Aunresolved&referrer=issue-stream&sort=freq

coltondemetriou added 2 commits November 24, 2025 12:40
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Walkthrough

The PR adds defensive checks and safer property access across InternalLayoutEditor, LayoutEditor, and ThemeEditor. It introduces a local current variable and guards for missing current.state in InternalLayoutEditor; filters out history entries without state before mapping in LayoutEditor and ThemeEditor; and returns early in the dev save flow if the computed history to send is missing. Control flow and public APIs remain unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI
  participant Editor as InternalLayoutEditor
  participant Histories
  participant DevServer as Dev/Save Flow

  Note over Editor,Histories: Load/derive current history
  UI->>Editor: trigger save/dev
  Editor->>Histories: const current = histories[index]
  alt current or current.state missing
    Editor-->>UI: return / abort (guard)
  else current.state present
    Editor->>DevServer: build payload using current.id and current.state.data/ui
    DevServer-->>UI: success/error
  end
Loading
sequenceDiagram
  autonumber
  participant UI
  participant Editor as Layout/Theme Editor
  participant LocalStorage as localHistories

  UI->>Editor: load initial history
  Editor->>LocalStorage: read localHistories
  LocalStorage-->>Editor: array (may contain items without state)
  Editor->>Editor: filter entries where h?.state
  alt no valid histories
    Editor-->>UI: do not assign puckInitialHistory (no early mutation)
  else valid histories exist
    Editor->>Editor: map to { id, state: migrate(data) }
    Editor-->>UI: set puckInitialHistory and index
  end
Loading

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: undefined state error' directly matches the main objective of the PR, which is to add safety checks for potentially null/undefined states to prevent Sentry errors.
Description check ✅ Passed The description clearly explains the purpose of the changes (adding safety checks for null/undefined states), mentions local testing, and references the related Sentry issue, all of which align with the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-state-main

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c98c893 and 7d70ff3.

📒 Files selected for processing (3)
  • packages/visual-editor/src/internal/components/InternalLayoutEditor.tsx (1 hunks)
  • packages/visual-editor/src/internal/components/LayoutEditor.tsx (2 hunks)
  • packages/visual-editor/src/internal/components/ThemeEditor.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/visual-editor/src/internal/components/InternalLayoutEditor.tsx
  • packages/visual-editor/src/internal/components/ThemeEditor.tsx
  • packages/visual-editor/src/internal/components/LayoutEditor.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: call_unit_test / unit_tests (20.x)
  • GitHub Check: call_unit_test / unit_tests (18.x)
  • GitHub Check: semgrep/ci

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/visual-editor/src/internal/components/ThemeEditor.tsx (1)

91-106: Keep InitialHistory.index aligned with the filtered histories

Here you filter localHistories down to entries with a state, but localHistoryIndex is still computed from the unfiltered localHistories array. If the last entry/entries are filtered out (e.g., state is null/undefined), localHistoryIndex can be greater than histories.length - 1, so Puck’s initialHistory.index no longer corresponds to an element in histories and may cause follow‑on undefined access elsewhere.

You’re already computing the index from the filtered list in LayoutEditor.tsx; I’d mirror that here:

-      const localHistoryIndex = localHistories.length - 1;
-      if (localHistoryIndex >= 0) {
-        devLogger.log("Theme Dev Mode - Using layout data from local storage");
-        const histories = localHistories
-          .filter((h) => h?.state)
-          .map((h) => {
-            // strip ui state
-            return { id: h.id, state: { data: { ...h.state.data } } };
-          });
+      const histories = localHistories
+        .filter((h) => h?.state)
+        .map((h) => {
+          // strip ui state
+          return { id: h.id, state: { data: { ...h.state.data } } };
+        });
+      const localHistoryIndex = histories.length - 1;
+      if (localHistoryIndex >= 0) {
+        devLogger.log("Theme Dev Mode - Using layout data from local storage");

[scratchpad_start]
[done]
ThemeEditor comment added.
[scratchpad_end] -->

🧹 Nitpick comments (3)
packages/visual-editor/src/internal/components/InternalLayoutEditor.tsx (1)

132-148: Early‑return when the selected history item has no state

Optional chaining here is good for avoiding the crash that Sentry was reporting, but if histories[index] is missing or has no state, we still enter this block and send a payload with hash/history fields based on undefined.

To keep downstream consumers from seeing half‑formed save‑state data, consider an explicit guard:

-        if (layoutSaveState?.hash !== histories[index]?.id) {
+        const current = histories[index];
+        if (!current?.state) {
+          return;
+        }
+
+        if (layoutSaveState?.hash !== current.id) {
           if (templateMetadata.isDevMode && !templateMetadata.devOverride) {
             devLogger.logFunc("sendDevSaveStateData");
             sendDevSaveStateData({
               payload: {
-                devSaveStateData: JSON.stringify(histories[index]?.state?.data),
+                devSaveStateData: JSON.stringify(current.state.data),
               },
             });
           } else {
             devLogger.logFunc("saveLayoutSaveState");
             saveLayoutSaveState({
               payload: {
-                hash: histories[index]?.id,
+                hash: current.id,
                 history: JSON.stringify({
-                  data: histories[index]?.state?.data,
-                  ui: histories[index]?.state?.ui,
+                  data: current.state.data,
+                  ui: current.state.ui,
                 }),
               },
             });
           }
         }
packages/visual-editor/src/internal/components/LayoutEditor.tsx (2)

148-169: Treat “all filtered histories” as no valid dev localStorage

The new filtering + migration is a nice hardening, but there’s an edge case:

  • localHistoryArray exists and parses, but every entry fails the !!history?.state filter.
  • localHistories.length becomes 0, so localHistoryIndex is -1, we skip the setPuckInitialHistory block but still set puckInitialHistoryFetched(true) and return;.

That leaves the editor with puckInitialHistory unset even though we do have layoutData available, effectively treating this as “empty layout” instead of “fall back to Content”.

If the intent is to ignore corrupted/invalid localStorage and use Content instead, you can only early‑return when at least one valid history remains:

-        const localHistories = (
-          JSON.parse(localHistoryArray) as History<AppState>[]
-        )
-          .filter((history): history is History<AppState> => !!history?.state)
-          .map((history) => ({
+        const localHistories = (
+          JSON.parse(localHistoryArray) as History<AppState>[]
+        )
+          .filter((history): history is History<AppState> => !!history?.state)
+          .map((history) => ({
             id: history.id,
             state: {
               data: migrate(history.state.data, migrationRegistry, puckConfig),
               ui: history.state.ui,
             },
           }));
         const localHistoryIndex = localHistories.length - 1;
-        if (localHistoryIndex >= 0) {
-          setPuckInitialHistory({
-            histories: localHistories as any,
-            index: localHistoryIndex,
-            appendData: false,
-          });
-        }
-        setPuckInitialHistoryFetched(true);
-        return;
+        if (localHistoryIndex >= 0) {
+          setPuckInitialHistory({
+            histories: localHistories as any,
+            index: localHistoryIndex,
+            appendData: false,
+          });
+          setPuckInitialHistoryFetched(true);
+          return;
+        }

This lets you naturally fall through to the “start fresh from Content” path when no valid histories remain.


255-262: Guard against sending devSaveStateData when no history is available

Optional chaining here prevents the previous “cannot read property of undefined” crash, but when there’s no history (or no state.data on the last history), historyToSend becomes undefined and we still send JSON.stringify(historyToSend).

To avoid pushing an undefined payload to the parent, add a simple guard:

-    const historyToSend =
-      puckInitialHistory?.histories?.[puckInitialHistory.histories.length - 1]
-        ?.state?.data;
-
-    devLogger.logFunc("sendDevSaveStateData useEffect");
-    sendDevLayoutSaveStateData({
-      payload: { devSaveStateData: JSON.stringify(historyToSend) },
-    });
+    const historyToSend =
+      puckInitialHistory?.histories?.[puckInitialHistory.histories.length - 1]
+        ?.state?.data;
+    if (!historyToSend) {
+      return;
+    }
+
+    devLogger.logFunc("sendDevSaveStateData useEffect");
+    sendDevLayoutSaveStateData({
+      payload: { devSaveStateData: JSON.stringify(historyToSend) },
+    });

This keeps the new null‑safety while avoiding unexpected “no‑data” payloads.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9b2250 and c98c893.

📒 Files selected for processing (3)
  • packages/visual-editor/src/internal/components/InternalLayoutEditor.tsx (1 hunks)
  • packages/visual-editor/src/internal/components/LayoutEditor.tsx (2 hunks)
  • packages/visual-editor/src/internal/components/ThemeEditor.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: call_unit_test / unit_tests (20.x)
  • GitHub Check: semgrep/ci

@colton-demetriou colton-demetriou changed the title fix: fix undefined state error fix: undefined state error Nov 24, 2025
@mkilpatrick
Copy link
Collaborator

The robot nits look good to implement. I'm wondering when this occurs though. Is the history in a bad state or has it not loaded properly yet?

@colton-demetriou
Copy link
Contributor Author

The robot nits look good to implement. I'm wondering when this occurs though. Is the history in a bad state or has it not loaded properly yet?

Good question, I'm not entirely sure how to tell. I feel like the most likely culprit is

puckInitialHistory?.histories?.[puckInitialHistory.histories.length - 1]
        ?.state?.data;

where histories is initialized as [], so the the index looks for -1, and then .state would be undefined -- not sure how to verify though

@colton-demetriou colton-demetriou merged commit 2c09fa1 into main Nov 24, 2025
14 checks passed
@colton-demetriou colton-demetriou deleted the fix-state-main branch November 24, 2025 20:22
This was referenced Dec 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants