Skip to content

Understanding the .watch() Method

What is .watch() and Why Do We Need It?

When building React applications, you often need to react to data changes but without re-rendering your component. This is where .watch() comes in handy.

The Problem .watch() Solves

tsx
// ❌ This approach re-renders the component every time count changes
function BadExample() {
  const count = sCount.use(); // This triggers re-render

  // Side effect - but component re-renders unnecessarily
  console.log("Count changed:", count);

  return <div>Some static content</div>; // Doesn't even use count!
}

// ✅ This approach only runs side effects, no re-render
function GoodExample() {
  // This runs side effect but doesn't re-render
  sCount.watch((count) => {
    console.log("Count changed:", count);
  });

  return <div>Some static content</div>; // Component stays the same
}

How .watch() Works Under the Hood

Let's understand the mechanism step by step:

Step 1: The Signature

tsx
signify.watch(callback, dependencies?)
  • callback: A function that receives the new value when it changes
  • dependencies: Optional array - when to re-setup the watcher

Step 2: Basic Usage

tsx
import { signify } from "react-signify";

const sCount = signify(0);

function MyComponent() {
  // ✅ Watch for side effects
  sCount.watch((value) => {
    console.log("Count changed:", value);
  });

  // Component doesn't re-render when sCount changes (only logs)
  return <div>Component content</div>;
}

What happens here:

  1. When MyComponent mounts, the watch callback is registered
  2. Whenever sCount changes, the callback runs with the new value
  3. The component itself never re-renders due to this change
  4. When the component unmounts, the callback is automatically cleaned up

Step 3: Automatic Cleanup (Memory Management)

One of the most powerful features of .watch() is automatic cleanup:

tsx
function MyComponent() {
  sData.watch((value) => {
    console.log("Data:", value);
  });

  return <div>Content</div>;
}

// ✅ When component unmounts:
// - Watcher automatically cleaned up
// - No memory leaks
// - Callback won't be called anymore

Step 4: Dependencies - When to Re-setup the Watcher

Sometimes your watch callback depends on other values that might change:

tsx
function MyComponent() {
  const [multiplier, setMultiplier] = useState(2);

  // ✅ Watch with dependencies
  sCount.watch(
    (value) => {
      console.log("Count * multiplier:", value * multiplier);
    },
    [multiplier]
  ); // Re-setup watcher when multiplier changes

  return (
    <div>
      <button onClick={() => setMultiplier((m) => m + 1)}>
        Update Multiplier: {multiplier}
      </button>
    </div>
  );
}

What happens with dependencies:

  1. Initial mount: Watch callback is set up with multiplier = 2
  2. When multiplier changes to 3:
    • Old callback (with multiplier = 2) is removed
    • New callback (with multiplier = 3) is registered
  3. Future count changes will use the latest multiplier value

Why dependencies are needed: Without dependencies, the callback would forever use the initial multiplier value due to JavaScript closures.

Common Use Cases for .watch()

Now that you understand how .watch() works, let's explore when and why you should use it:

1. Logging and Debugging

When to use: You want to track changes for debugging or monitoring purposes.

Why .watch() is perfect: You don't need to re-render anything, just log the changes.

tsx
const sUserActions = signify([]);

function App() {
  // ✅ Log all user actions for debugging
  sUserActions.watch((actions) => {
    console.log("User actions:", actions);

    // Send to analytics service
    if (actions.length > 0) {
      analytics.track("user_action", {
        action: actions[actions.length - 1],
        total_actions: actions.length,
      });
    }
  });

  return <AppContent />;
}

// Somewhere else in your app:
// sUserActions.set(prev => [...prev.value, 'clicked_button']);
// This will trigger the watch callback but won't re-render App component

2. Data Persistence (Auto-save)

When to use: You want to automatically save data to localStorage, database, or server when it changes.

Why .watch() is perfect: Saving is a side effect that doesn't need to affect the UI immediately.

tsx
const sUserPreferences = signify({
  theme: "light",
  language: "en",
  fontSize: 14,
});

function App() {
  // ✅ Auto-save preferences to localStorage
  sUserPreferences.watch((preferences) => {
    localStorage.setItem("userPrefs", JSON.stringify(preferences));
    console.log("Preferences saved!"); // User doesn't see this in UI
  });

  // ✅ Load initial preferences
  useEffect(() => {
    const saved = localStorage.getItem("userPrefs");
    if (saved) {
      sUserPreferences.set(JSON.parse(saved));
    }
  }, []);

  return <AppContent />;
}

// When user changes theme:
// sUserPreferences.set(prev => ({ ...prev.value, theme: 'dark' }));
// This automatically saves to localStorage without re-rendering App

3. API Synchronization

When to use: You want to sync local changes with a server or external API.

Why .watch() is perfect: API calls are side effects that shouldn't block UI updates.

tsx
const sShoppingCart = signify([]);

function App() {
  // ✅ Sync cart with server whenever it changes
  sShoppingCart.watch(async (cart) => {
    try {
      await saveCartToServer(cart);
      console.log("Cart saved to server");
    } catch (error) {
      console.error("Failed to save cart:", error);
      // Could show a toast notification here
    }
  });

  return <AppContent />;
}

// When user adds item to cart:
// sShoppingCart.set(prev => [...prev.value, newItem]);
// This triggers automatic sync to server without blocking UI

4. Cross-Component Communication

When to use: You want one piece of data to automatically update another when it changes.

Why .watch() is perfect: You're transforming data from one form to another as a side effect.

tsx
const sNotifications = signify([]);
const sToastMessages = signify([]);

function App() {
  // ✅ Transform notifications into toast messages
  sNotifications.watch((notifications) => {
    const newToasts = notifications
      .filter((n) => n.type === "info") // Only info notifications become toasts
      .map((n) => ({
        id: n.id,
        message: n.message,
        type: "toast",
      }));

    sToastMessages.set(newToasts);
  });

  return <AppContent />;
}

// Flow: User gets notification → Automatically becomes toast → UI shows toast
// The App component never re-renders, but ToastContainer (using sToastMessages.use()) will

5. Real-time Form Validation

When to use: You want to validate form data as the user types without re-rendering the entire form.

Why .watch() is perfect: Validation is a side effect that updates error state independently.

tsx
const sFormData = signify({
  email: "",
  password: "",
  confirmPassword: "",
});

const sFormErrors = signify({});

function LoginForm() {
  // ✅ Validate form whenever data changes
  sFormData.watch((data) => {
    const errors = {};

    // Email validation
    if (data.email && !data.email.includes("@")) {
      errors.email = "Invalid email format";
    }

    // Password validation
    if (data.password && data.password.length < 8) {
      errors.password = "Password must be at least 8 characters";
    }

    // Confirm password validation
    if (data.confirmPassword && data.password !== data.confirmPassword) {
      errors.confirmPassword = "Passwords do not match";
    }

    sFormErrors.set(errors);
  });

  return <FormContent />;
}

// Flow: User types → sFormData changes → Validation runs → sFormErrors updates → Error UI updates
// The validation logic doesn't cause LoginForm to re-render

Advanced Patterns

Once you're comfortable with basic .watch() usage, here are some advanced techniques:

1. Debounced Side Effects

Problem: You want to react to changes, but only after the user stops making changes for a while (like search-as-you-type).

Solution: Use debouncing with cleanup functions.

tsx
const sSearchQuery = signify("");
const sSearchResults = signify([]);

function SearchComponent() {
  // ✅ Debounced API calls - only search after user stops typing
  sSearchQuery.watch((query) => {
    const timeoutId = setTimeout(async () => {
      if (query.trim()) {
        try {
          const results = await searchAPI(query);
          sSearchResults.set(results);
        } catch (error) {
          console.error("Search failed:", error);
        }
      } else {
        sSearchResults.set([]); // Clear results for empty query
      }
    }, 500); // Wait 500ms after last keystroke

    // ✅ Cleanup function: cancel previous timeout when new change comes
    return () => clearTimeout(timeoutId);
  });

  return <SearchUI />;
}

// How it works:
// User types "a" → timeout starts
// User types "ap" → previous timeout cancelled, new timeout starts
// User types "app" → previous timeout cancelled, new timeout starts
// User stops typing → after 500ms, API call is made with "app"

Key insight: The cleanup function is crucial to prevent multiple API calls.

2. Conditional Watching

Problem: You want to watch changes only under certain conditions.

Solution: Use dependencies to re-setup the watcher when conditions change.

tsx
const sUser = signify(null);
const sUserActivity = signify([]);

function App() {
  const [isTracking, setIsTracking] = useState(false);

  // ✅ Conditional watching with dependencies
  sUserActivity.watch(
    (activity) => {
      if (isTracking && activity.length > 0) {
        // Only track when isTracking = true
        analytics.track("user_activity", {
          actions: activity,
          timestamp: new Date(),
        });
      }
    },
    [isTracking]
  ); // Re-setup when isTracking changes

  return (
    <div>
      <button onClick={() => setIsTracking(!isTracking)}>
        {isTracking ? "Stop" : "Start"} Tracking
      </button>
    </div>
  );
}

// How it works:
// 1. Initially isTracking = false, watcher is set up but does nothing
// 2. User clicks "Start" → isTracking = true → watcher is re-setup with new condition
// 3. Now when sUserActivity changes, analytics tracking actually happens
// 4. User clicks "Stop" → isTracking = false → watcher re-setup again, analytics stops

Alternative approach - Better performance:

tsx
// ✅ Even better: Only watch when needed
sUserActivity.watch(
  (activity) => {
    // Always send to analytics when watcher is active
    analytics.track("user_activity", {
      actions: activity,
      timestamp: new Date(),
    });
  },
  isTracking ? [] : undefined
); // Only setup watcher when isTracking is true

3. Multiple Signify Coordination

Problem: You have multiple related pieces of state that need to stay in sync.

Solution: Use .watch() to coordinate between different signify instances.

tsx
const sUser = signify(null);
const sCart = signify([]);
const sWishlist = signify([]);

function App() {
  // ✅ Clear cart and wishlist when user logs out
  sUser.watch((user) => {
    if (!user) {
      // User logged out
      sCart.set([]);
      sWishlist.set([]);

      // Clear related localStorage
      localStorage.removeItem("cart");
      localStorage.removeItem("wishlist");
    }
  });

  // ✅ Sync cart changes with user preferences
  sCart.watch((cart) => {
    const user = sUser.value; // Access current user
    if (user) {
      syncCartWithUserPreferences(user.id, cart);
    }
  });

  return <AppContent />;
}

// Flow examples:
// 1. User logs out → sUser becomes null → cart and wishlist auto-clear
// 2. User adds item to cart → sCart changes → auto-sync with user preferences
// 3. Different components can modify sCart, all changes auto-sync

4. State Machine Transitions

Problem: Your app has different states (loading, authenticated, error) and you need to perform different actions when transitioning between them.

Solution: Use .watch() to react to state transitions and trigger appropriate side effects.

tsx
type AppState = "idle" | "loading" | "authenticated" | "error";

const sAppState = signify<AppState>("idle");
const sUser = signify(null);
const sError = signify(null);

function App() {
  // ✅ Watch state transitions and handle side effects
  sAppState.watch((state) => {
    console.log("App state changed to:", state);

    switch (state) {
      case "loading":
        // Show loading indicator, disable forms
        document.body.style.cursor = "wait";
        break;

      case "authenticated":
        // Initialize user-specific data
        loadUserData();
        document.body.style.cursor = "default";
        break;

      case "error":
        // Handle error state
        showErrorNotification();
        document.body.style.cursor = "default";
        break;

      case "idle":
        // Reset app state
        sUser.set(null);
        sError.set(null);
        document.body.style.cursor = "default";
        break;
    }
  });

  return <AppContent />;
}

// Usage examples:
// sAppState.set('loading'); // Triggers loading side effects
// sAppState.set('authenticated'); // Triggers auth side effects
// sAppState.set('error'); // Triggers error handling

5. Performance Monitoring

tsx
const sPageViews = signify([]);
const sUserInteractions = signify([]);

function App() {
  // ✅ Monitor performance metrics
  sPageViews.watch((views) => {
    const lastView = views[views.length - 1];
    if (lastView) {
      // Track page load time
      performance.mark("page-view-end");
      performance.measure("page-load", "page-view-start", "page-view-end");

      const measure = performance.getEntriesByName("page-load")[0];
      analytics.track("page_load_time", {
        page: lastView.path,
        duration: measure.duration,
      });
    }
  });

  // ✅ Track user engagement
  sUserInteractions.watch((interactions) => {
    const recentInteractions = interactions.filter(
      (i) => Date.now() - i.timestamp < 60000 // Last minute
    );

    if (recentInteractions.length > 10) {
      analytics.track("high_engagement", {
        interactions_per_minute: recentInteractions.length,
      });
    }
  });

  return <AppContent />;
}

Integration with External Libraries

1. Analytics Integration

tsx
const sUserEvents = signify([]);

function App() {
  // ✅ Send events to multiple analytics services
  sUserEvents.watch((events) => {
    const latestEvent = events[events.length - 1];
    if (latestEvent) {
      // Google Analytics
      gtag("event", latestEvent.name, {
        event_category: latestEvent.category,
        event_label: latestEvent.label,
      });

      // Mixpanel
      mixpanel.track(latestEvent.name, latestEvent.properties);

      // Custom analytics
      customAnalytics.track(latestEvent);
    }
  });

  return <AppContent />;
}

2. WebSocket Integration

tsx
const sRealtimeData = signify(null);
const sConnectionStatus = signify("disconnected");

function App() {
  // ✅ Send data via WebSocket when there are changes
  sRealtimeData.watch((data) => {
    const connectionStatus = sConnectionStatus.value;

    if (connectionStatus === "connected" && data) {
      websocket.send(
        JSON.stringify({
          type: "data_update",
          payload: data,
          timestamp: Date.now(),
        })
      );
    }
  });

  // ✅ Handle connection status changes
  sConnectionStatus.watch((status) => {
    if (status === "connected") {
      console.log("WebSocket connected");
      // Resend pending data
      const currentData = sRealtimeData.value;
      if (currentData) {
        websocket.send(
          JSON.stringify({
            type: "sync_data",
            payload: currentData,
          })
        );
      }
    } else {
      console.log("WebSocket disconnected");
    }
  });

  return <AppContent />;
}

3. Browser APIs Integration

tsx
const sGeolocation = signify(null);
const sNotificationPermission = signify("default");

function App() {
  // ✅ Watch location changes
  sGeolocation.watch((location) => {
    if (location) {
      // Update weather based on location
      updateWeatherForLocation(location);

      // Store in localStorage for offline use
      localStorage.setItem("lastKnownLocation", JSON.stringify(location));
    }
  });

  // ✅ Handle notification permission changes
  sNotificationPermission.watch((permission) => {
    if (permission === "granted") {
      // Enable push notifications
      registerServiceWorker();
    } else if (permission === "denied") {
      // Fall back to in-app notifications
      console.log("Notifications disabled, using in-app alerts");
    }
  });

  return <AppContent />;
}

Error Handling

1. Safe Error Handling

tsx
const sApiData = signify(null);

function App() {
  // ✅ Handle errors in watch callbacks
  sApiData.watch((data) => {
    try {
      if (data) {
        // Process data
        processData(data);

        // Update other states
        updateRelatedStates(data);
      }
    } catch (error) {
      console.error("Error processing data:", error);

      // Set error state
      sErrorState.set({
        message: "Failed to process data",
        timestamp: new Date(),
      });
    }
  });

  return <AppContent />;
}

2. Async Error Handling

tsx
const sUserPreferences = signify({});

function App() {
  // ✅ Handle async errors
  sUserPreferences.watch(async (preferences) => {
    try {
      await savePreferencesToServer(preferences);
    } catch (error) {
      console.error("Failed to save preferences:", error);

      // Show user-friendly error
      sToastMessages.set((prev) => {
        prev.value.push({
          id: Date.now(),
          type: "error",
          message: "Failed to save preferences. Changes saved locally.",
        });
      });

      // Retry logic
      setTimeout(() => {
        sUserPreferences.set(preferences); // Trigger retry
      }, 5000);
    }
  });

  return <AppContent />;
}

Performance Considerations

1. Avoid Heavy Computations

tsx
const sLargeDataset = signify([]);

function App() {
  // ❌ Expensive computation on every change
  sLargeDataset.watch((data) => {
    const expensiveResult = performHeavyComputation(data); // Slow!
    console.log(expensiveResult);
  });

  // ✅ Debounce expensive operations
  sLargeDataset.watch((data) => {
    const timeoutId = setTimeout(() => {
      const result = performHeavyComputation(data);
      console.log(result);
    }, 1000); // Debounce 1s

    return () => clearTimeout(timeoutId);
  });

  return <AppContent />;
}

2. Conditional Processing

tsx
const sUserActivity = signify([]);

function App() {
  // ✅ Process only when necessary
  sUserActivity.watch((activity) => {
    // Only process if significant change
    if (activity.length % 10 === 0) {
      // Every 10 actions
      batchProcessActivity(activity);
    }

    // Only process recent activity
    const recentActivity = activity.slice(-5); // Last 5 actions
    updateRecentActivityIndicator(recentActivity);
  });

  return <AppContent />;
}

Testing

1. Testing Watch Callbacks

tsx
// Component
function UserTracker() {
  sUser.watch((user) => {
    if (user) {
      analytics.track("user_login", { userId: user.id });
    }
  });

  return <div>User Tracker</div>;
}

// Test
describe("UserTracker", () => {
  beforeEach(() => {
    sUser.reset();
    jest.clearAllMocks();
  });

  it("should track user login", () => {
    const mockTrack = jest.spyOn(analytics, "track");

    render(<UserTracker />);

    // Trigger user login
    sUser.set({ id: 1, name: "John" });

    expect(mockTrack).toHaveBeenCalledWith("user_login", {
      userId: 1,
    });
  });
});

2. Testing Async Watch Callbacks

tsx
// Component
function DataSync() {
  sData.watch(async (data) => {
    await saveToServer(data);
  });

  return <div>Data Sync</div>;
}

// Test
describe("DataSync", () => {
  it("should save data to server", async () => {
    const mockSave = jest.fn().mockResolvedValue(true);
    saveToServer.mockImplementation(mockSave);

    render(<DataSync />);

    // Trigger data change
    sData.set({ key: "value" });

    // Wait for async operation
    await waitFor(() => {
      expect(mockSave).toHaveBeenCalledWith({ key: "value" });
    });
  });
});

Best Practices

Understanding when and how to use .watch() properly is crucial for building efficient React applications.

✅ Do's - What You Should Do

1. Use for Side Effects Only

tsx
// ✅ Perfect use cases for .watch()
sData.watch((data) => {
  console.log(data); // Logging - doesn't affect UI
  saveToLocalStorage(data); // Persistence - background operation
  sendToAnalytics(data); // Analytics - tracking events
});

Why this works: These are all side effects that don't need to trigger component re-renders.

2. Handle Async Operations Safely

tsx
// ✅ Always wrap async operations in try-catch
sData.watch(async (data) => {
  try {
    await processData(data);
    console.log("Data processed successfully");
  } catch (error) {
    console.error("Processing failed:", error);
    // Show user-friendly error message
    showToast("Failed to save data");
  }
});

Why this matters: Unhandled async errors can crash your app or create silent failures.

3. Use Dependencies When Your Callback Needs External Values

tsx
// ✅ Include external values in dependencies
const [apiEndpoint, setApiEndpoint] = useState("/api/v1");

sData.watch(
  (data) => {
    saveToAPI(apiEndpoint, data); // Uses external variable
  },
  [apiEndpoint]
); // Re-setup when endpoint changes

Why dependencies matter: Without them, your callback would forever use stale values.

4. Return Cleanup Functions When Needed

tsx
// ✅ Clean up subscriptions, timers, etc.
sData.watch((data) => {
  const subscription = websocket.subscribe(data.topic);
  const timer = setInterval(() => ping(), 1000);

  // Cleanup function runs when component unmounts or dependencies change
  return () => {
    subscription.unsubscribe();
    clearInterval(timer);
  };
});

❌ Don'ts - Common Mistakes to Avoid

1. Don't Use for Rendering Data

tsx
// ❌ Wrong - This is what .use() is for
sData.watch((data) => {
  setComponent(<div>{data}</div>); // Causes unnecessary complexity
});

// ✅ Correct - Use .use() for rendering
function Component() {
  const data = sData.use(); // Automatically triggers re-render
  return <div>{data}</div>;
}

Why this is wrong: .watch() doesn't trigger re-renders, so your UI won't update properly.

2. Don't Forget Error Handling in Async Callbacks

tsx
// ❌ Dangerous - Errors will be unhandled
sData.watch(async (data) => {
  await riskyOperation(data); // What if this throws?
});

// ✅ Safe - Always handle errors
sData.watch(async (data) => {
  try {
    await riskyOperation(data);
  } catch (error) {
    handleError(error);
  }
});

3. Don't Perform Heavy Synchronous Operations

tsx
// ❌ Blocks the UI thread
sData.watch((data) => {
  const result = heavyComputation(data); // Takes 2 seconds!
  console.log(result);
});

// ✅ Use debouncing or async processing
sData.watch((data) => {
  const timeoutId = setTimeout(() => {
    const result = heavyComputation(data);
    console.log(result);
  }, 500);

  return () => clearTimeout(timeoutId);
});

4. Don't Create Infinite Loops

tsx
// ❌ Infinite loop - A updates B, B updates A
sDataA.watch((dataA) => {
  sDataB.set(processA(dataA));
});
sDataB.watch((dataB) => {
  sDataA.set(processB(dataB)); // This triggers the first watcher again!
});

// ✅ Use conditions to break the loop
sDataA.watch((dataA) => {
  const newB = processA(dataA);
  if (newB !== sDataB.value) {
    // Only update if different
    sDataB.set(newB);
  }
});

Comparison with Other Methods

Understanding when to use .watch() vs other methods is crucial for making the right choice.

.watch() vs .use() - Key Differences

Feature.watch().use()
Triggers re-render❌ No✅ Yes
Best for side effects✅ Perfect❌ Wrong tool
Returns current value❌ No✅ Yes
Automatic cleanup✅ Yes✅ Yes
Use in components✅ Yes✅ Yes

When to Use Each:

tsx
function Component() {
  // ✅ Use .use() for displaying data (causes re-render)
  const data = sData.use();

  // ✅ Use .watch() for side effects (no re-render)
  sData.watch((data) => {
    console.log("Data changed:", data);
    sendToAnalytics(data);
  });

  return <div>{data}</div>; // UI updates automatically when data changes
}

Quick decision guide:

  • Need to show data in UI? → Use .use()
  • Need to react to changes without affecting UI? → Use .watch()

.watch() vs useEffect with Dependencies

Many developers wonder when to use .watch() vs useEffect. Here's a clear comparison:

The Old Way (useEffect)

tsx
// ❌ More complex and error-prone
function Component() {
  const data = sData.use(); // This causes re-render

  useEffect(() => {
    console.log("Data changed:", data);
    // If you have more complex logic, you need to manually track dependencies
  }, [data]); // Must manually maintain dependency array

  return <div>{data}</div>; // Component re-renders when data changes
}

Problems with this approach:

  1. Component re-renders even if you only need side effects
  2. Must manually maintain dependency array
  3. Easy to forget dependencies (causing stale closures)
  4. Mixes rendering concerns with side effect concerns

The New Way (.watch())

tsx
// ✅ Cleaner and more efficient
function Component() {
  // Only watch for side effects - no re-render
  sData.watch((data) => {
    console.log("Data changed:", data);
    // Automatically tracks the right dependencies
  }); // No manual dependency management needed

  return <div>Component</div>; // Component doesn't re-render unless needed
}

Benefits of .watch():

  1. ✅ No unnecessary re-renders
  2. ✅ Automatic dependency tracking
  3. ✅ Cleaner separation of concerns
  4. ✅ Less boilerplate code

When to Use Each:

Use CaseRecommended Approach
Side effects only (logging, analytics).watch()
Side effects + need current value in UI.use() + .watch()
React to multiple signify instancesMultiple .watch() calls
Complex dependency management.watch() with dependencies

TypeScript Support

React Signify provides excellent TypeScript support with full type safety for .watch() callbacks:

tsx
interface User {
  id: number;
  name: string;
  preferences: {
    theme: "light" | "dark";
  };
}

const sUser = signify<User | null>(null);

function App() {
  // ✅ Type-safe watch callback
  sUser.watch((user) => {
    // TypeScript knows: user is User | null
    if (user) {
      // ✅ Full type checking and autocomplete
      console.log(`User ${user.name} logged in`);

      // ✅ Type-safe property access
      analytics.track("login", {
        userId: user.id,
        theme: user.preferences.theme, // TypeScript validates this exists
      });
    }
  });

  // ✅ Type-safe dependencies
  const [multiplier, setMultiplier] = useState(2);

  sUser.watch(
    (user) => {
      if (user) {
        const score = user.id * multiplier; // All types are known
        console.log("User score:", score);
      }
    },
    [multiplier]
  ); // Dependencies are type-checked too

  return <AppContent />;
}

TypeScript benefits:

  • Full type inference for callback parameters
  • Autocomplete for properties and methods
  • Compile-time error checking prevents runtime bugs
  • Type-safe dependencies array validation

Summary

Congratulations! You now understand how .watch() works and when to use it. Let's recap everything:

What is .watch()?

.watch() is a method that lets you react to state changes without causing component re-renders. It's perfect for side effects like logging, API calls, data persistence, and other background operations.

How It Works

  1. Setup: When your component mounts, .watch() registers your callback
  2. Execution: When the signify state changes, your callback runs with the new value
  3. Cleanup: When the component unmounts, the callback is automatically removed
  4. Dependencies: Optionally re-setup the watcher when dependencies change

Core Use Cases

Use CaseExampleWhy .watch() is Perfect
Logging & Analyticsconsole.log(), tracking eventsNo UI impact needed
Data PersistenceSave to localStorage/serverBackground operation
API SynchronizationSync with external servicesAsync side effect
Cross-Component CommunicationUpdate related stateCoordination logic
Form ValidationReal-time validationUpdates separate error state

Key Benefits

  • No unnecessary re-renders - Component stays unchanged
  • Automatic cleanup - No memory leaks or zombie callbacks
  • Simple dependency management - No manual arrays to maintain
  • TypeScript support - Full type safety and autocomplete
  • Error handling - Built-in support for async operations

Quick Decision Guide

Use .watch() when you need to:

  • React to changes without affecting the UI
  • Perform side effects (logging, persistence, analytics)
  • Coordinate between different pieces of state
  • Handle background operations

Use .use() when you need to:

  • Display data in your component's UI
  • Trigger component re-renders
  • Access the current value for rendering

The Golden Rule

Use .watch() for side effects, .use() for rendering data.

Next Steps

Now that you understand .watch(), you can:

  1. Replace unnecessary useEffect calls with cleaner .watch() calls
  2. Build more efficient components that don't re-render needlessly
  3. Create better separation between UI logic and side effect logic
  4. Write more maintainable React applications

💡 Remember: .watch() is your tool for side effects without re-renders!

Released under the MIT License.