useStore()

React hook for subscribing to reactive proxy state with fine-grained updates.

The useStore() hook is Mesa's React integration that allows components to subscribe to specific parts of proxy state. It ensures components only re-render when their selected data actually changes.

Syntax

function useStore<T extends object, R = T>(store: T, selector?: (state: T) => R): R;

Parameters

  • store - A proxy object created with proxy()
  • selector (optional) - A function that extracts the data your component needs. If omitted, returns the entire store.

Returns

Returns the selected data from the store. The component will re-render only when this data changes.

Basic Usage

Entire Store Subscription

import { proxy, useStore } from "mesa-react";

const state = proxy({
  count: 0,
  name: "John",
});

function AppComponent() {
  // Subscribe to entire store - re-renders when any property changes
  const store = useStore(state);

  return (
    <div>
      <span>Count: {store.count}</span>
      <span>Name: {store.name}</span>
      <button onClick={() => store.count++}>Increment</button>
    </div>
  );
}

// Equivalent to:
function AppComponentExplicit() {
  const store = useStore(state, (s) => s);
  return <div>...</div>;
}

Simple Property Selection

function Counter() {
  // Only re-renders when count changes
  const count = useStore(state, (s) => s.count);

  return (
    <div>
      <span>Count: {count}</span>
      <button onClick={() => state.count++}>Increment</button>
    </div>
  );
}

Nested Property Selection

const state = proxy({
  user: {
    name: "John",
    profile: {
      age: 25,
      email: "john@example.com",
    },
  },
});

function UserProfile() {
  // Only re-renders when user.name changes
  const name = useStore(state, (s) => s.user.name);

  // Only re-renders when user.profile.age changes
  const age = useStore(state, (s) => s.user.profile.age);

  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
    </div>
  );
}

Advanced Selection Patterns

Object Selection

function UserCard() {
  // Re-renders when any property of user changes
  const user = useStore(state, (s) => s.user);

  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.profile.email}</p>
    </div>
  );
}

Array Selection

const state = proxy({
  todos: [
    { id: 1, text: "Learn Mesa", done: false },
    { id: 2, text: "Build app", done: false },
  ],
});

function TodoList() {
  // Re-renders when todos array changes
  const todos = useStore(state, (s) => s.todos);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.text} - {todo.done ? "Done" : "Pending"}
        </li>
      ))}
    </ul>
  );
}

function TodoCount() {
  // Only re-renders when array length changes
  const count = useStore(state, (s) => s.todos.length);

  return <div>Total: {count}</div>;
}

Computed Values

function TodoStats() {
  // Re-renders when relevant todo properties change
  const stats = useStore(state, (s) => ({
    total: s.todos.length,
    completed: s.todos.filter((t) => t.done).length,
    pending: s.todos.filter((t) => !t.done).length,
  }));

  return (
    <div>
      <p>Total: {stats.total}</p>
      <p>Completed: {stats.completed}</p>
      <p>Pending: {stats.pending}</p>
    </div>
  );
}

TypeScript Support

Mesa provides full type safety with TypeScript:

interface AppState {
  count: number;
  user: {
    name: string;
    age: number;
  };
  items: string[];
}

const state = proxy<AppState>({
  count: 0,
  user: { name: "John", age: 25 },
  items: ["a", "b", "c"],
});

function TypedComponent() {
  // TypeScript infers return type as number
  const count = useStore(state, (s) => s.count);

  // TypeScript infers return type as string
  const name = useStore(state, (s) => s.user.name);

  // TypeScript infers return type as string[]
  const items = useStore(state, (s) => s.items);

  return (
    <div>
      {count} - {name} - {items.length}
    </div>
  );
}

Performance Optimization

Selector Stability

The selector function should be stable to avoid unnecessary subscriptions:

// ✅ Good - selector is stable
function Component() {
  const name = useStore(state, (s) => s.user.name);
  return <div>{name}</div>;
}

// ❌ Avoid - creates new selector each render
function Component() {
  const user = useStore(state, (s) => ({
    name: s.user.name,
    age: s.user.age,
  }));
  return <div>{user.name}</div>;
}

// ✅ Better - use useCallback for complex selectors
function Component() {
  const selector = useCallback(
    (s) => ({
      name: s.user.name,
      age: s.user.age,
    }),
    []
  );

  const user = useStore(state, selector);
  return <div>{user.name}</div>;
}

Fine-Grained Subscriptions

Mesa automatically tracks which properties your selector accesses:

function OptimalComponent() {
  // Only subscribes to count changes
  const count = useStore(state, (s) => s.count);

  // Only subscribes to user.name changes
  const name = useStore(state, (s) => s.user.name);

  // This component won't re-render when user.age changes
  return (
    <div>
      {count} - {name}
    </div>
  );
}

Common Patterns

Multiple Subscriptions

function Dashboard() {
  const user = useStore(state, (s) => s.user);
  const stats = useStore(state, (s) => s.stats);
  const notifications = useStore(state, (s) => s.notifications);

  return (
    <div>
      <UserSection user={user} />
      <StatsSection stats={stats} />
      <NotificationSection notifications={notifications} />
    </div>
  );
}

Conditional Selection

function ConditionalComponent() {
  const isLoggedIn = useStore(state, (s) => s.user.isLoggedIn);

  // Only access user data if logged in
  const userData = useStore(state, (s) => (s.user.isLoggedIn ? s.user.profile : null));

  if (!isLoggedIn) {
    return <LoginForm />;
  }

  return <UserProfile user={userData} />;
}

Array Item Selection

function TodoItem({ todoId }) {
  // Only re-renders when this specific todo changes
  const todo = useStore(state, (s) => s.todos.find((t) => t.id === todoId));

  if (!todo) return null;

  return (
    <div>
      <span>{todo.text}</span>
      <button onClick={() => (todo.done = !todo.done)}>Toggle</button>
    </div>
  );
}

Best Practices

✅ Do

// Use specific selectors
const name = useStore(state, (s) => s.user.name);

// Group related data
const userInfo = useStore(state, (s) => ({
  name: s.user.name,
  email: s.user.email,
}));

// Use multiple specific subscriptions
const count = useStore(state, (s) => s.count);
const status = useStore(state, (s) => s.status);

❌ Don't

// Don't create new objects unnecessarily in selectors
const data = useStore(state, (s) => ({
  ...s.user,
  computed: s.other.value * 2,
})); // ❌ Creates new object each time

// Don't use complex computations in selectors without memoization
const expensive = useStore(state, (s) => s.items.map((item) => expensiveTransform(item))); // ❌ Runs on every render

// Instead, use useMemo for expensive computations
const expensive = useMemo(() => items.map((item) => expensiveTransform(item)), [items]);

Integration with React DevTools

Mesa integrates with React DevTools for debugging:

// Components show their Mesa dependencies
function DebugComponent() {
  const count = useStore(state, (s) => s.count);
  const name = useStore(state, (s) => s.user.name);

  // React DevTools will show:
  // - Component re-renders
  // - Mesa state subscriptions
  return (
    <div>
      {count} - {name}
    </div>
  );
}

See Also