Skip to content

Conversation

@fpena
Copy link
Contributor

@fpena fpena commented Oct 16, 2025

What does this PR do?

This PR implements the state snapshots feature for Reactotron, allowing developers to capture, store, and restore application state snapshots.

The feature adds a new "Snapshots" tab to the existing State screen, enabling users to take snapshots of their Redux or MobX-State-Tree store state with a single click, view them in an expandable card layout, and restore any captured state back to the connected client.

"Snapshots" enhances the debugging experience by providing state persistence across sessions, enabling developers to capture state at specific points in time and restore it later for testing scenarios or bug reproduction. The implementation includes proper error handling, type safety, and integrates seamlessly with Reactotron's existing WebSocket communication system.

Key Features:

  • 📸 Snapshot Creation: One-click state capture from Redux/MobX-State-Tree stores
  • 💾 State Storage: Local storage with automatic deduplication
  • 🔄 State Restoration: Restore any snapshot to connected clients
  • 📋 Copy Operations: Copy individual or all snapshots to clipboard
  • 🗑️ Snapshot Management: Delete unwanted snapshots
  • 🎨 Modern UI: Card-based layout with expandable content
  • Smart Naming: Human-readable timestamps (e.g., "Wednesday @ 5:00:15 PM")
  • 🛡️ Error Handling: Robust validation and error management
  • 🔗 Client Targeting: Restore to specific clients or active client

⚠️ Known Limitations:

  • Snapshot Renaming: The ability to rename snapshots has not been included due to some modifications the patches within the patches folder are making. These patches modify TextInput behavior in React Native macOS, causing issues with text input functionality. This feature will be addressed in a separate PR once the TextInput issues are resolved.

What GitHub issues does this PR fix?

#41

How to verify this code works?

reactotron-macos setup for this PR

  1. gh pr checkout <PR_NUMBER>
  2. npm install
  3. npm run pod
  4. npm run start
  5. npm run macos

reactotron example app setup for this PR

  1. yarn install
    Add Reactotron standalone server port to example app configuration
diff --git a/apps/example-app/app/devtools/ReactotronConfig.ts b/apps/example-app/app/devtools/ReactotronConfig.ts
index a5c7d5b6..9b9c1dad 100644
--- a/apps/example-app/app/devtools/ReactotronConfig.ts
+++ b/apps/example-app/app/devtools/ReactotronConfig.ts
@@ -18,6 +18,7 @@ import { Reactotron } from "./ReactotronClient"

 const reactotron = Reactotron.configure({
   name: require("../../package.json").name,
+  port: 9292,
   onConnect: () => {
     /** since this file gets hot reloaded, let's clear the past logs every time we connect */
     Reactotron.clear()
  1. yarn workspace example-app start
  2. Start on a different port to avoid conflicting with reactotron-macos
  3. Press i to launch in iOS simulator
  4. Consider Cmd + Shift + R to reload reactotron-macos to get connection

Step by step of how to test this PR

Prerequisites

  • Ensure you have a React Native app connected to Reactotron
  • The app should be using Redux, MobX-State-Tree, or another state management library
  • Reactotron server should be running and connected

Testing Steps

📸 Snapshot Creation

  • Navigate to the State screen in Reactotron
  • Click on the "Snapshots" tab
  • Click the "Create Snapshot" button
  • Verify that a new snapshot appears in the list with a timestamp-based name (e.g., "Wednesday @ 5:00:15 PM")
  • Test creating multiple snapshots to verify deduplication works

🔍 Snapshot Interaction

  • Expand/Collapse: Click on any snapshot card to expand and view the state tree
  • Copy Individual: Click the clipboard icon on a snapshot to copy it to clipboard
  • Copy All: Click the "Copy All" button to copy all snapshots to clipboard
  • Delete: Click the trash icon to delete a snapshot

🔄 State Restoration

  • Create a snapshot of your current app state
  • Make changes to your app's state (e.g., update Redux store, modify MobX state)
  • In Reactotron, click the restore icon (arrow up) on the snapshot
  • Verify that your app's state has been restored to the captured state
  • Test restoration with different clients if multiple are connected

🎨 UI/UX Testing

  • Tab Switching: Switch between "Subscriptions" and "Snapshots" tabs
  • Empty State: Test the empty state message when no snapshots exist
  • Tooltips: Hover over action buttons to verify tooltips appear

🧪 Edge Cases

  • No Active Client: Try creating snapshots when no client is connected
  • Invalid State: Test with corrupted or invalid state data
  • Multiple Clients: Test snapshot creation/restoration with multiple connected clients
  • Large State: Test with large state objects to verify performance

🔧 Technical Validation

  • Deduplication: Create snapshots rapidly to test duplicate prevention
  • Memory Management: Create and delete many snapshots to test cleanup
  • Error Handling: Test with network disconnections during snapshot operations

Expected Results
✅ Snapshots are created with proper timestamps
✅ State restoration works correctly
✅ Copy operations work (check clipboard)
✅ UI is responsive and intuitive
✅ Error handling works for edge cases
✅ No memory leaks with repeated operations

Screenshot/Video of "How to test this PR?"

CleanShot.2025-10-23.at.20.20.30.mp4

@fpena fpena changed the title [DRAFT] Snapshots feat: Snapshots Oct 24, 2025
@fpena fpena changed the title feat: Snapshots feat: Snapshots (without snapshot renaming) Oct 24, 2025
@fpena fpena marked this pull request as ready for review October 24, 2025 03:33
Copy link
Member

@jamonholmgren jamonholmgren left a comment

Choose a reason for hiding this comment

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

Some things to address, but headed the right direction

Comment on lines 54 to 57
if (!activeTab) {
console.log("No active client to create snapshot from")
return
}
Copy link
Member

Choose a reason for hiding this comment

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

Prefer one-liner guards if possible.

Suggested change
if (!activeTab) {
console.log("No active client to create snapshot from")
return
}
if (!activeTab) return console.log("No active client to create snapshot from")


const copySnapshotToClipboard = (snapshot: Snapshot) => {
try {
IRClipboard.setString(JSON.stringify(snapshot.state, null, 2))
Copy link
Member

Choose a reason for hiding this comment

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

Should we be formatting the JSON string like this, or just stringifying without formatting?

Copy link
Member

Choose a reason for hiding this comment

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

I think this should be exported to a utility function in another file rather than putting it inline in the component body. It doesn't depend on any state except what's passed in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jamonholmgren

Should we be formatting the JSON string like this, or just stringifying without formatting?

Is your concern here about performance and/or storage usage?

} catch (error) {
console.error("Failed to copy snapshots to clipboard:", error)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This can be extracted to a utility function as well. You can either pass in the snapshots to the function or just get them on-demand with withGlobal (or if @SeanBarker182 has renamed that function, whatever he landed on).

>
<Text>Clear State</Text>
</Pressable>
<>
Copy link
Member

Choose a reason for hiding this comment

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

Why is the ScrollView wrapped in a Fragment, here? Looks like it's just one element.

<Tab id="snapshots" label="Snapshots" tabgroup="activeStateTab" />
</View>
<View style={$stateContainer()}>
{activeStateTab === "Subscriptions" ? (
Copy link
Member

Choose a reason for hiding this comment

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

I think we're at the point where this should be refactored into two sub components. Likely something like StateSubscriptions and StateSnapshots.

@jamonholmgren
Copy link
Member

Add screenshots of the feature too, please

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