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
// ❌ 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
signify.watch(callback, dependencies?)
callback
: A function that receives the new value when it changesdependencies
: Optional array - when to re-setup the watcher
Step 2: Basic Usage
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:
- When
MyComponent
mounts, the watch callback is registered - Whenever
sCount
changes, the callback runs with the new value - The component itself never re-renders due to this change
- 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:
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:
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:
- Initial mount: Watch callback is set up with
multiplier = 2
- When
multiplier
changes to3
:- Old callback (with
multiplier = 2
) is removed - New callback (with
multiplier = 3
) is registered
- Old callback (with
- 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.
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.
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.
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.
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.
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.
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.
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:
// ✅ 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.
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.
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
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
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
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
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
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
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
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
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
// 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
// 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
// ✅ 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
// ✅ 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
// ✅ 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
// ✅ 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
// ❌ 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
// ❌ 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
// ❌ 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
// ❌ 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:
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)
// ❌ 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:
- Component re-renders even if you only need side effects
- Must manually maintain dependency array
- Easy to forget dependencies (causing stale closures)
- Mixes rendering concerns with side effect concerns
The New Way (.watch())
// ✅ 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()
:
- ✅ No unnecessary re-renders
- ✅ Automatic dependency tracking
- ✅ Cleaner separation of concerns
- ✅ Less boilerplate code
When to Use Each:
Use Case | Recommended Approach |
---|---|
Side effects only (logging, analytics) | .watch() |
Side effects + need current value in UI | .use() + .watch() |
React to multiple signify instances | Multiple .watch() calls |
Complex dependency management | .watch() with dependencies |
TypeScript Support
React Signify provides excellent TypeScript support with full type safety for .watch()
callbacks:
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
- Setup: When your component mounts,
.watch()
registers your callback - Execution: When the signify state changes, your callback runs with the new value
- Cleanup: When the component unmounts, the callback is automatically removed
- Dependencies: Optionally re-setup the watcher when dependencies change
Core Use Cases
Use Case | Example | Why .watch() is Perfect |
---|---|---|
Logging & Analytics | console.log() , tracking events | No UI impact needed |
Data Persistence | Save to localStorage/server | Background operation |
API Synchronization | Sync with external services | Async side effect |
Cross-Component Communication | Update related state | Coordination logic |
Form Validation | Real-time validation | Updates 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:
- Replace unnecessary
useEffect
calls with cleaner.watch()
calls - Build more efficient components that don't re-render needlessly
- Create better separation between UI logic and side effect logic
- Write more maintainable React applications
💡 Remember: .watch()
is your tool for side effects without re-renders!