useWatch

Execute side effects when reactive data changes without triggering component re-renders

API Reference

Syntax

const currentValue = useWatch(watchSource, callback, options?)

Parameters

  • watchSource: Store instance, computed function, or array of sources to watch
  • callback: Function executed when watched data changes (can return cleanup function)
  • options (optional):
    • immediate: Execute callback immediately with current value (default: false)
    • once: Execute callback only once, then stop watching (default: false)
    • deep: Enable deep equality checking for nested objects (default: false)

Returns

  • currentValue: The current value of the watched source

Configuration Options

Immediate Execution

useWatch(
  () => userStore.value.preferences,
  (preferences) => {
    applyUserPreferences(preferences);
  },
  { immediate: true } // Applies preferences immediately on building the component
);

One-time Watchers

useWatch(
  () => userStore.value.isLoggedIn,
  (isLoggedIn) => {
    if (isLoggedIn) {
      console.log("First login detected!");
      showWelcomeMessage();
    }
  },
  { once: true } // Only triggers once
);

Deep Equality Checking

const configStore = createStore({
  api: {
    endpoints: { users: "/api/users", orders: "/api/orders" },
    timeout: 5000,
  },
});

useWatch(
  () => configStore.value.api,
  (apiConfig, prevApiConfig) => {
    console.log("API configuration changed");
    reinitializeApiClient(apiConfig);
  },
  { deep: true } // Deep comparison prevents unnecessary triggers
);

Examples

Watch Store Changes

Track store changes for logging, analytics, or external API calls:

const counterStore = createStore({ count: 0 });

function Counter() {
  // Side effect only - no re-renders triggered
  useWatch(counterStore, (current, prev) => {
    console.log(`Count: ${prev.count} → ${current.count}`);

    // Track analytics
    analytics.track("counter_changed", {
      from: prev.count,
      to: current.count,
    });
  });

  return (
    <button onClick={() => counterStore.setState((s) => s.count++)}>
      Increment
    </button>
  );
}

Watch Computed Values

Monitor derived data for business logic:

const userStore = createStore({
  firstName: "John",
  lastName: "Doe",
  age: 30,
});

function UserTracker() {
  // Watch full name changes
  useWatch(
    () => `${userStore.value.firstName} ${userStore.value.lastName}`,
    (fullName, prevFullName) => {
      if (prevFullName) {
        console.log(`Name updated: "${prevFullName}" → "${fullName}"`);
        api.updateUserProfile({ fullName });
      }
    }
  );

  // Watch age milestones
  useWatch(
    () => userStore.value.age,
    (age, prevAge) => {
      if (age >= 18 && prevAge < 18) {
        console.log("User became an adult!");
        unlockAdultFeatures();
      }
    }
  );

  return <div>Tracking user changes...</div>;
}

Watch Multiple Sources

Monitor changes across multiple stores simultaneously:

const userStore = createStore({ name: "John", role: "user" });
const settingsStore = createStore({ theme: "light", language: "en" });

function AppWatcher() {
  useWatch(
    () => [userStore.value, settingsStore.value] as const,
    ([user, settings], [prevUser, prevSettings]) => {
      // Role-based logic
      if (user.role !== prevUser?.role) {
        console.log(`Role changed: ${prevUser?.role} → ${user.role}`);
        redirectBasedOnRole(user.role);
      }

      // Theme persistence
      if (settings.theme !== prevSettings?.theme) {
        console.log(`Theme: ${prevSettings?.theme} → ${settings.theme}`);
        document.body.className = settings.theme;
        localStorage.setItem("theme", settings.theme);
      }
    }
  );

  return <div>Monitoring app state...</div>;
}

Cleanup Functions

Perfect for managing subscriptions, timers, and external resources:

function WebSocketManager() {
  useWatch(
    () => userStore.value.connectionId,
    (connectionId, prevConnectionId) => {
      if (!connectionId) return;

      console.log(`Connecting to: ${connectionId}`);
      const ws = new WebSocket(`ws://api.com/${connectionId}`);

      ws.onopen = () => console.log("Connected");
      ws.onmessage = (event) => handleMessage(event.data);
      ws.onerror = (error) => console.error("WebSocket error:", error);

      // Cleanup function - automatically called on next change or unmount
      return () => {
        console.log(`Disconnecting from: ${connectionId}`);
        ws.close();
      };
    }
  );

  return <div>WebSocket manager active</div>;
}

Best Practices

Use for side effects only

// Good: External side effects
useWatch(
  () => store.value.theme,
  (theme) => {
    document.body.className = theme;
    localStorage.setItem("theme", theme);
  }
);

Don't use for component state

// Bad: Use useStoreValue instead
const [displayValue, setDisplayValue] = useState("");
useWatch(
  () => store.value.data,
  (data) => {
    setDisplayValue(data); // This creates unnecessary indirection
  }
);

Be specific with watchers

// Good: Watch specific fields
useWatch(
  () => userStore.value.email,
  (email) => {
    validateEmail(email);
  }
);

Avoid watching large objects

// Bad: Triggers on any store change
useWatch(
  () => largeStore.value,
  (store) => {
    console.log("Something changed"); // Too broad
  }
);

Always clean up resources

// Good: Proper cleanup
useWatch(
  () => store.value.intervalMs,
  (ms) => {
    const interval = setInterval(doSomething, ms);
    return () => clearInterval(interval);
  }
);

Hook Comparison

HookPurposeRe-rendersAuto-trackingBest For
useWatchManual dependency watching❌ No❌ NoSpecific side effects
useWatchEffectAuto-tracked side effects❌ No✅ YesGeneral side effects
useStoreValueComponent state sync✅ Yes❌ NoUI state
useComputedDerived reactive values✅ Yes✅ YesCalculated data