Counter Example

A comprehensive counter example showcasing Mesa's fine-grained reactivity.

This example demonstrates Mesa's core concepts through a simple counter application that showcases fine-grained updates, multiple component subscriptions, and performance benefits.

🚀 Live Interactive Playground

Experience Mesa's fine-grained reactivity with our interactive counter playground:

The playground features:

  • Visual render tracking with console logs
  • Independent component updates demonstration
  • Performance monitoring in real-time
  • Interactive controls for step size and actions
  • Action history with timestamps
  • Statistics dashboard showing operation counts

Live Example

Here's a complete counter application built with Mesa:

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

// Create reactive state
const counterState = proxy({
  count: 0,
  step: 1,
  history: [],
  stats: {
    increments: 0,
    decrements: 0,
    resets: 0,
  },
  initialized: false,
});

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

  console.log("CounterDisplay rendered");

  return (
    <div className="counter-display">
      <h2>Count: {count}</h2>
    </div>
  );
}

// Control buttons component
function CounterControls() {
  // Only re-renders when step changes
  const step = useStore(counterState, (s) => s.step);

  console.log("CounterControls rendered");

  const increment = () => {
    counterState.count += step;
    counterState.stats.increments++;
    counterState.history.push({
      action: "increment",
      value: step,
      timestamp: Date.now(),
    });
  };

  const decrement = () => {
    counterState.count -= step;
    counterState.stats.decrements++;
    counterState.history.push({
      action: "decrement",
      value: step,
      timestamp: Date.now(),
    });
  };

  const reset = () => {
    counterState.count = 0;
    counterState.stats.resets++;
    counterState.history.push({
      action: "reset",
      value: 0,
      timestamp: Date.now(),
    });
  };

  return (
    <div className="counter-controls">
      <button onClick={increment}>+{step}</button>
      <button onClick={decrement}>-{step}</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

// Step size selector
function StepSelector() {
  // Only re-renders when step changes
  const step = useStore(counterState, (s) => s.step);

  console.log("StepSelector rendered");

  return (
    <div className="step-selector">
      <label>
        Step size:
        <select value={step} onChange={(e) => (counterState.step = Number(e.target.value))}>
          <option value={1}>1</option>
          <option value={5}>5</option>
          <option value={10}>10</option>
          <option value={100}>100</option>
        </select>
      </label>
    </div>
  );
}

// Statistics component
function CounterStats() {
  // Only re-renders when stats change
  const stats = useStore(counterState, (s) => s.stats);

  console.log("CounterStats rendered");

  const total = stats.increments + stats.decrements + stats.resets;

  return (
    <div className="counter-stats">
      <h3>Statistics</h3>
      <div>Increments: {stats.increments}</div>
      <div>Decrements: {stats.decrements}</div>
      <div>Resets: {stats.resets}</div>
      <div>Total actions: {total}</div>
    </div>
  );
}

// History component
function CounterHistory() {
  // Only re-renders when history changes
  const history = useStore(counterState, (s) => s.history);

  console.log("CounterHistory rendered");

  const recentHistory = history.slice(-5); // Show last 5 actions

  return (
    <div className="counter-history">
      <h3>Recent History</h3>
      {recentHistory.length === 0 ? (
        <p>No actions yet</p>
      ) : (
        <ul>
          {recentHistory.map((action, index) => (
            <li key={`${action.timestamp}-${index}`}>
              {action.action} {action.value} at {new Date(action.timestamp).toLocaleTimeString()}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

// Main app component
function CounterApp() {
  console.log("CounterApp rendered");

  return (
    <div className="counter-app">
      <h1>Mesa Counter Example</h1>
      <CounterDisplay />
      <StepSelector />
      <CounterControls />
      <div className="counter-info">
        <CounterStats />
        <CounterHistory />
      </div>
    </div>
  );
}

export default CounterApp;

Initialization with useInitSync

For more advanced use cases, you can use useInitSync to initialize your counter with data from localStorage, user preferences, or API calls:

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

// Enhanced counter state with initialization
const enhancedCounterState = proxy({
  count: 0,
  step: 1,
  history: [],
  stats: {
    increments: 0,
    decrements: 0,
    resets: 0,
  },
  userPreferences: {
    theme: "light",
    defaultStep: 1,
  },
  initialized: false,
  loading: true,
});

// Mock functions for demonstration
const loadUserPreferences = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      // Simulate loading from localStorage or API
      const saved = localStorage.getItem('counter-preferences');
      if (saved) {
        resolve(JSON.parse(saved));
      } else {
        resolve({
          theme: "light",
          defaultStep: 1,
          savedCount: 0,
        });
      }
    }, 300);
  });
};

function EnhancedCounterApp() {
  // Initialize counter with saved data
  useInitSync(enhancedCounterState, async (state) => {
    state.loading = true;
    
    try {
      const preferences = await loadUserPreferences();
      
      // Apply loaded preferences
      state.count = preferences.savedCount || 0;
      state.step = preferences.defaultStep || 1;
      state.userPreferences = preferences;
      state.initialized = true;
      
    } catch (error) {
      console.error("Failed to load preferences:", error);
      // Use defaults on error
      state.initialized = true;
    } finally {
      state.loading = false;
    }
  });

  const { loading, initialized } = useStore(enhancedCounterState, s => ({
    loading: s.loading,
    initialized: s.initialized
  }));

  if (loading) {
    return (
      <div className="counter-app">
        <div className="loading-state">
          <div>🔄 Loading counter...</div>
        </div>
      </div>
    );
  }

  return (
    <div className="counter-app">
      <h1>Enhanced Counter with Initialization</h1>
      <EnhancedCounterDisplay />
      <EnhancedCounterControls />
      <PreferencesPanel />
    </div>
  );
}

function EnhancedCounterDisplay() {
  const count = useStore(enhancedCounterState, s => s.count);
  const theme = useStore(enhancedCounterState, s => s.userPreferences.theme);
  
  return (
    <div className={`counter-display theme-${theme}`}>
      <h2>Count: {count}</h2>
    </div>
  );
}

function EnhancedCounterControls() {
  const step = useStore(enhancedCounterState, s => s.step);
  
  const increment = () => {
    enhancedCounterState.count += step;
    enhancedCounterState.stats.increments++;
    saveToLocalStorage();
  };
  
  const decrement = () => {
    enhancedCounterState.count -= step;
    enhancedCounterState.stats.decrements++;
    saveToLocalStorage();
  };
  
  const reset = () => {
    enhancedCounterState.count = 0;
    enhancedCounterState.stats.resets++;
    saveToLocalStorage();
  };
  
  return (
    <div className="counter-controls">
      <button onClick={increment}>+{step}</button>
      <button onClick={decrement}>-{step}</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

function PreferencesPanel() {
  const { theme, defaultStep } = useStore(enhancedCounterState, s => s.userPreferences);
  const step = useStore(enhancedCounterState, s => s.step);
  
  const updateTheme = (newTheme) => {
    enhancedCounterState.userPreferences.theme = newTheme;
    saveToLocalStorage();
  };
  
  const updateStep = (newStep) => {
    enhancedCounterState.step = newStep;
    enhancedCounterState.userPreferences.defaultStep = newStep;
    saveToLocalStorage();
  };
  
  return (
    <div className="preferences-panel">
      <h3>Preferences</h3>
      <div className="preference-group">
        <label>Theme:</label>
        <select value={theme} onChange={(e) => updateTheme(e.target.value)}>
          <option value="light">Light</option>
          <option value="dark">Dark</option>
        </select>
      </div>
      <div className="preference-group">
        <label>Step Size:</label>
        <select value={step} onChange={(e) => updateStep(Number(e.target.value))}>
          <option value={1}>1</option>
          <option value={5}>5</option>
          <option value={10}>10</option>
        </select>
      </div>
    </div>
  );
}

// Helper function to save state to localStorage
function saveToLocalStorage() {
  const dataToSave = {
    savedCount: enhancedCounterState.count,
    defaultStep: enhancedCounterState.userPreferences.defaultStep,
    theme: enhancedCounterState.userPreferences.theme,
  };
  localStorage.setItem('counter-preferences', JSON.stringify(dataToSave));
}

Key Initialization Patterns

1. Loading from localStorage

useInitSync(counterState, (state) => {
  // Load saved count from localStorage
  const savedCount = localStorage.getItem('counter-value');
  if (savedCount) {
    state.count = parseInt(savedCount, 10);
  }
  
  // Load user preferences
  const preferences = JSON.parse(localStorage.getItem('counter-prefs') || '{}');
  state.step = preferences.defaultStep || 1;
});

2. Async Data Loading

useInitSync(counterState, async (state) => {
  state.loading = true;
  
  try {
    // Fetch user's counter settings from API
    const response = await fetch('/api/user/counter-settings');
    const settings = await response.json();
    
    state.count = settings.startValue || 0;
    state.step = settings.preferredStep || 1;
  } catch (error) {
    console.error('Failed to load settings:', error);
    // Use defaults
  } finally {
    state.loading = false;
  }
});

3. Conditional Initialization

function CounterWithUser({ userId }) {
  useInitSync(counterState, async (state) => {
    if (!userId) {
      // Anonymous user - use defaults
      state.count = 0;
      state.step = 1;
      return;
    }
    
    // Logged-in user - load their settings
    const userSettings = await fetchUserCounterSettings(userId);
    state.count = userSettings.lastCount;
    state.step = userSettings.preferredStep;
    state.history = userSettings.recentHistory;
  }, {
    deps: [userId] // Re-run when userId changes
  });
}

## What This Example Demonstrates

### 1. Fine-Grained Updates

Each component only re-renders when its specific data changes:

- **CounterDisplay**: Only when `count` changes
- **CounterControls**: Only when `step` changes
- **StepSelector**: Only when `step` changes
- **CounterStats**: Only when `stats` object changes
- **CounterHistory**: Only when `history` array changes

### 2. Independent State Updates

Try these actions and observe the console logs:

```jsx
// Only CounterDisplay re-renders
counterState.count = 42;

// Only StepSelector and CounterControls re-render
counterState.step = 5;

// Only CounterStats re-renders
counterState.stats.increments++;

// Only CounterHistory re-renders
counterState.history.push({ action: "test", value: 0, timestamp: Date.now() });

3. Nested Object Updates

The example shows how nested updates work:

// Updates stats.increments - only CounterStats re-renders
counterState.stats.increments++;

// Updates entire stats object - CounterStats re-renders
counterState.stats = { increments: 0, decrements: 0, resets: 0 };

4. Array Operations

History tracking demonstrates array reactivity:

// All of these only trigger CounterHistory re-renders
counterState.history.push(newAction); // Add item
counterState.history.pop(); // Remove last item
counterState.history[0] = updatedAction; // Update item
counterState.history.splice(1, 1); // Remove item

Performance Comparison

Without Mesa (Traditional State)

// All components re-render on any state change
const [state, setState] = useState({
  count: 0,
  step: 1,
  history: [],
  stats: { increments: 0, decrements: 0, resets: 0 },
});

// Changing count triggers ALL component re-renders
setState((prev) => ({ ...prev, count: prev.count + 1 }));

With Mesa

// Only relevant components re-render
const state = proxy({
  count: 0,
  step: 1,
  history: [],
  stats: { increments: 0, decrements: 0, resets: 0 },
});

// Only CounterDisplay re-renders
state.count++;

Advanced Patterns

Computed Values

function ComputedStats() {
  // Re-renders when count or stats change
  const computedData = useStore(counterState, (s) => ({
    count: s.count,
    total: s.stats.increments + s.stats.decrements + s.stats.resets,
    average: s.history.length > 0 ? s.history.reduce((sum, action) => sum + action.value, 0) / s.history.length : 0,
  }));

  return (
    <div>
      <div>Current: {computedData.count}</div>
      <div>Total actions: {computedData.total}</div>
      <div>Average step: {computedData.average.toFixed(2)}</div>
    </div>
  );
}

Conditional Subscriptions

function ConditionalDisplay() {
  const hasHistory = useStore(counterState, (s) => s.history.length > 0);

  // Only subscribes to history when it has items
  const lastAction = useStore(counterState, (s) => (s.history.length > 0 ? s.history[s.history.length - 1] : null));

  return <div>{hasHistory ? <div>Last action: {lastAction?.action}</div> : <div>No actions yet</div>}</div>;
}

Custom Hooks

function useCounterActions() {
  return {
    increment: (step = 1) => {
      counterState.count += step;
      counterState.stats.increments++;
      counterState.history.push({
        action: "increment",
        value: step,
        timestamp: Date.now(),
      });
    },

    decrement: (step = 1) => {
      counterState.count -= step;
      counterState.stats.decrements++;
      counterState.history.push({
        action: "decrement",
        value: step,
        timestamp: Date.now(),
      });
    },

    reset: () => {
      counterState.count = 0;
      counterState.stats.resets++;
      counterState.history.push({
        action: "reset",
        value: 0,
        timestamp: Date.now(),
      });
    },
  };
}

function CounterWithHooks() {
  const count = useStore(counterState, (s) => s.count);
  const actions = useCounterActions();

  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => actions.increment(5)}>+5</button>
      <button onClick={() => actions.decrement(3)}>-3</button>
      <button onClick={actions.reset}>Reset</button>
    </div>
  );
}

Testing

import { render, screen, fireEvent } from "@testing-library/react";
import { proxy, useStore } from "mesa-react";

describe("Counter with Mesa", () => {
  let testState;

  beforeEach(() => {
    testState = proxy({
      count: 0,
      step: 1,
    });
  });

  test("counter increments correctly", () => {
    function TestCounter() {
      const count = useStore(testState, (s) => s.count);
      return (
        <div>
          <div data-testid="count">{count}</div>
          <button onClick={() => testState.count++}>Increment</button>
        </div>
      );
    }

    render(<TestCounter />);

    expect(screen.getByTestId("count")).toHaveTextContent("0");

    fireEvent.click(screen.getByText("Increment"));

    expect(screen.getByTestId("count")).toHaveTextContent("1");
  });

  test("independent updates work correctly", () => {
    let countRenders = 0;
    let stepRenders = 0;

    function CountDisplay() {
      const count = useStore(testState, (s) => s.count);
      countRenders++;
      return <div data-testid="count">{count}</div>;
    }

    function StepDisplay() {
      const step = useStore(testState, (s) => s.step);
      stepRenders++;
      return <div data-testid="step">{step}</div>;
    }

    render(
      <div>
        <CountDisplay />
        <StepDisplay />
      </div>
    );

    // Initial renders
    expect(countRenders).toBe(1);
    expect(stepRenders).toBe(1);

    // Update count - only CountDisplay should re-render
    testState.count = 5;
    expect(countRenders).toBe(2);
    expect(stepRenders).toBe(1);

    // Update step - only StepDisplay should re-render
    testState.step = 10;
    expect(countRenders).toBe(2);
    expect(stepRenders).toBe(2);
  });
});

This counter example demonstrates Mesa's key benefits: fine-grained updates, performance optimization, and clean, reactive code patterns.

See Also