JB logo

Command Palette

Search for a command to run...

yOUTUBE
Blog
Next

Simple Store - The State Management Library React Developers Have Been Waiting For

Master React state management with Simple Store. Learn how to build production-ready shopping carts and authentication systems with granular selectors, zero boilerplate, and just 220 lines of code. A complete guide with TypeScript examples.

Simple Store: The State Management Library React Developers Have Been Waiting For

State management in React has always been a point of contention. Should you use Redux with its boilerplate? Zustand with its action functions? Jotai with its atoms? What if I told you there's a library that combines the best parts of all these solutions into something beautifully simple?

Meet Simple Store – a reactive state management solution that gives you the granular control of Zustand with the simplicity of Jotai, all in just 220 lines of code.

Why Switch to Simple Store?

The Problem with Existing Solutions

Let's be honest about the pain points:

Zustand requires you to:

  • Define types upfront for your stores
  • Collocate actions inside the store
  • Write verbose setter functions with object spreading

Jotai gives you:

  • Simple atom creation with type inference
  • But requires complex patterns for selecting nested values
  • Poor documentation around granular subscriptions

Redux demands:

  • Mountains of boilerplate
  • Separate actions, reducers, and selectors
  • A learning curve that scares away beginners

What Simple Store Offers

Simple Store delivers the best of all worlds:

Type inference – No need to define types upfront
Granular selectors – Subscribe only to what you need
Direct mutations – No action functions required
Chainable selects – Access deeply nested properties with ease
Zero boilerplate – Get started in seconds
Framework agnostic core – Works anywhere, with React hooks included
Tiny bundle size – Just 220 lines of code

Installation & Setup

Step 1: Install the Package

pnpm add @simplestack/store

Or with your preferred package manager:

pnpm add @simplestack/store
# or
yarn add @simplestack/store

Step 2: Import and Create Your First Store

import { store } from "@simplestack/store";
import { useStoreValue } from "@simplestack/store/react";
 
const counterStore = store(0);
 
function Counter() {
  const count = useStoreValue(counterStore);
 
  return (
    <button onClick={() => counterStore.set(count + 1)}>
      Count: {count}
    </button>
  );
}

That's it! No providers, no context, no boilerplate. Just create a store and use it.

Core Concepts

Before we dive into real-world examples, let's understand the fundamentals:

1. Creating Stores

// Primitives
const nameStore = store("John");
const ageStore = store(25);
 
// Objects (with full type inference)
const userStore = store({
  name: "John",
  email: "john@example.com",
  preferences: {
    theme: "dark",
    notifications: true,
  },
});

2. Reading Values

// Get current value
const currentName = nameStore.get();
 
// Subscribe to changes in React
const name = useStoreValue(nameStore);

3. Setting Values

// Direct value
nameStore.set("Jane");
 
// Function updater
nameStore.set((prev) => prev.toUpperCase());

4. Selecting Parts of a Store

This is where Simple Store shines:

// Select a specific property
const themeStore = userStore.select("preferences").select("theme");
 
// Now you can operate on just the theme
themeStore.set("light");
 
// Components only re-render when theme changes
const theme = useStoreValue(themeStore);

Real-World Example 1: Shopping Cart

Let's build a production-ready shopping cart that handles adding items, updating quantities, and calculating totals.

Step 1: Define Your Store

// stores/cartStore.ts
import { store } from "@simplestack/store";
 
type CartItem = {
  id: string;
  name: string;
  price: number;
  quantity: number;
  image?: string;
};
 
type CartState = {
  items: CartItem[];
  total: number;
};
 
export const cartStore = store<CartState>({
  items: [],
  total: 0,
});
 
// Export selected stores for granular updates
export const cartItemsStore = cartStore.select("items");
export const cartTotalStore = cartStore.select("total");

Step 2: Create Helper Functions

// stores/cartStore.ts (continued)
 
const calculateTotal = (items: CartItem[]) =>
  items.reduce((sum, item) => sum + item.price * item.quantity, 0);
 
export const addToCart = (product: Omit<CartItem, "quantity">) => {
  const currentItems = cartItemsStore.get();
  const existingItem = currentItems.find((item) => item.id === product.id);
 
  let newItems: CartItem[];
 
  if (existingItem) {
    // Increase quantity if item exists
    newItems = currentItems.map((item) =>
      item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
    );
  } else {
    // Add new item with quantity 1
    newItems = [...currentItems, { ...product, quantity: 1 }];
  }
 
  // Update both items and total
  cartItemsStore.set(newItems);
  cartTotalStore.set(calculateTotal(newItems));
};
 
export const removeFromCart = (productId: string) => {
  const currentItems = cartItemsStore.get();
  const newItems = currentItems.filter((item) => item.id !== productId);
 
  cartItemsStore.set(newItems);
  cartTotalStore.set(calculateTotal(newItems));
};
 
export const updateQuantity = (productId: string, quantity: number) => {
  if (quantity <= 0) {
    removeFromCart(productId);
    return;
  }
 
  const currentItems = cartItemsStore.get();
  const newItems = currentItems.map((item) =>
    item.id === productId ? { ...item, quantity } : item
  );
 
  cartItemsStore.set(newItems);
  cartTotalStore.set(calculateTotal(newItems));
};
 
export const clearCart = () => {
  cartStore.set({ items: [], total: 0 });
};

Step 3: Build Your Components

// components/CartBadge.tsx
import { useStoreValue } from "@simplestack/store/react";
import { cartItemsStore } from "../stores/cartStore";
 
export function CartBadge() {
  // Only subscribes to items array - won't re-render when total changes
  const items = useStoreValue(cartItemsStore);
  const itemCount = items?.reduce((sum, item) => sum + item.quantity, 0) ?? 0;
 
  if (itemCount === 0) return null;
 
  return (
    <div className="cart-badge">
      {itemCount}
    </div>
  );
}
// components/Cart.tsx
import { useStoreValue } from "@simplestack/store/react";
import {
  cartItemsStore,
  cartTotalStore,
  updateQuantity,
  removeFromCart,
  clearCart
} from "../stores/cartStore";
 
export function Cart() {
  const items = useStoreValue(cartItemsStore);
  const total = useStoreValue(cartTotalStore);
 
  if (!items || items.length === 0) {
    return <div className="cart-empty">Your cart is empty</div>;
  }
 
  return (
    <div className="cart">
      <h2>Shopping Cart</h2>
 
      <div className="cart-items">
        {items.map((item) => (
          <div key={item.id} className="cart-item">
            {item.image && <img src={item.image} alt={item.name} />}
 
            <div className="cart-item-details">
              <h3>{item.name}</h3>
              <p className="price">${item.price.toFixed(2)}</p>
            </div>
 
            <div className="cart-item-actions">
              <input
                type="number"
                value={item.quantity}
                onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}
                min="1"
              />
              <button onClick={() => removeFromCart(item.id)}>
                Remove
              </button>
            </div>
 
            <div className="cart-item-total">
              ${(item.price * item.quantity).toFixed(2)}
            </div>
          </div>
        ))}
      </div>
 
      <div className="cart-footer">
        <div className="cart-total">
          <strong>Total:</strong>
          <span>${total?.toFixed(2)}</span>
        </div>
 
        <div className="cart-actions">
          <button onClick={clearCart} className="btn-secondary">
            Clear Cart
          </button>
          <button className="btn-primary">
            Checkout
          </button>
        </div>
      </div>
    </div>
  );
}
// components/ProductCard.tsx
import { addToCart } from "../stores/cartStore";
 
type Product = {
  id: string;
  name: string;
  price: number;
  image?: string;
};
 
export function ProductCard({ product }: { product: Product }) {
  return (
    <div className="product-card">
      {product.image && <img src={product.image} alt={product.name} />}
      <h3>{product.name}</h3>
      <p className="price">${product.price.toFixed(2)}</p>
      <button onClick={() => addToCart(product)}>
        Add to Cart
      </button>
    </div>
  );
}

Why This Approach Works

Performance: Each component only subscribes to the data it needs. The CartBadge won't re-render when the total changes, and vice versa.

Simplicity: No context providers, no prop drilling, no action creators. Just import the store and use it.

Type Safety: Full TypeScript support with inference means fewer bugs and better developer experience.

Real-World Example 2: Authentication Management

Now let's tackle something more complex – a complete authentication system with login, logout, and protected routes.

Step 1: Define Your Auth Store

// stores/authStore.ts
import { store } from "@simplestack/store";
 
type User = {
  id: string;
  email: string;
  name: string;
  avatar?: string;
  role: "user" | "admin";
};
 
type AuthState = {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
};
 
export const authStore = store<AuthState>({
  user: null,
  isAuthenticated: false,
  isLoading: true,
  error: null,
});
 
// Export granular selectors
export const userStore = authStore.select("user");
export const isAuthenticatedStore = authStore.select("isAuthenticated");
export const isLoadingStore = authStore.select("isLoading");
export const errorStore = authStore.select("error");

Step 2: Create Authentication Actions

// stores/authStore.ts (continued)
 
export const login = async (email: string, password: string) => {
  // Set loading state
  isLoadingStore.set(true);
  errorStore.set(null);
 
  try {
    const response = await fetch("/api/auth/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password }),
    });
 
    if (!response.ok) {
      throw new Error("Invalid credentials");
    }
 
    const data = await response.json();
 
    // Update store with user data
    userStore.set(data.user);
    isAuthenticatedStore.set(true);
 
    // Persist token
    localStorage.setItem("authToken", data.token);
 
    return data.user;
  } catch (error) {
    const message = error instanceof Error ? error.message : "Login failed";
    errorStore.set(message);
    throw error;
  } finally {
    isLoadingStore.set(false);
  }
};
 
export const logout = async () => {
  isLoadingStore.set(true);
 
  try {
    await fetch("/api/auth/logout", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${localStorage.getItem("authToken")}`,
      },
    });
  } catch (error) {
    console.error("Logout error:", error);
  } finally {
    // Clear auth state
    authStore.set({
      user: null,
      isAuthenticated: false,
      isLoading: false,
      error: null,
    });
 
    // Clear persisted token
    localStorage.removeItem("authToken");
  }
};
 
export const checkAuth = async () => {
  const token = localStorage.getItem("authToken");
 
  if (!token) {
    isLoadingStore.set(false);
    return;
  }
 
  isLoadingStore.set(true);
 
  try {
    const response = await fetch("/api/auth/me", {
      headers: { Authorization: `Bearer ${token}` },
    });
 
    if (!response.ok) {
      throw new Error("Session expired");
    }
 
    const user = await response.json();
 
    // Restore user session
    userStore.set(user);
    isAuthenticatedStore.set(true);
  } catch (error) {
    // Clear invalid token
    localStorage.removeItem("authToken");
    errorStore.set("Session expired. Please login again.");
  } finally {
    isLoadingStore.set(false);
  }
};
 
export const updateProfile = async (updates: Partial<User>) => {
  const currentUser = userStore.get();
  if (!currentUser) return;
 
  // Optimistic update - update UI immediately
  const optimisticUser = { ...currentUser, ...updates };
  userStore.set(optimisticUser);
 
  try {
    const response = await fetch("/api/auth/profile", {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${localStorage.getItem("authToken")}`,
      },
      body: JSON.stringify(updates),
    });
 
    if (!response.ok) {
      throw new Error("Update failed");
    }
 
    const updatedUser = await response.json();
    userStore.set(updatedUser);
  } catch (error) {
    // Revert optimistic update on error
    userStore.set(currentUser);
    errorStore.set("Failed to update profile");
    throw error;
  }
};

Step 3: Create Auth Components

// components/AuthProvider.tsx
import { useEffect } from "react";
import { useStoreValue } from "@simplestack/store/react";
import { isLoadingStore, checkAuth } from "../stores/authStore";
 
export function AuthProvider({ children }: { children: React.ReactNode }) {
  const isLoading = useStoreValue(isLoadingStore);
 
  useEffect(() => {
    // Check authentication status on mount
    checkAuth();
  }, []);
 
  if (isLoading) {
    return (
      <div className="auth-loading">
        <div className="spinner" />
        <p>Loading...</p>
      </div>
    );
  }
 
  return <>{children}</>;
}
// components/LoginForm.tsx
import { useState } from "react";
import { useStoreValue } from "@simplestack/store/react";
import { login, errorStore, isLoadingStore } from "../stores/authStore";
 
export function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
 
  const error = useStoreValue(errorStore);
  const isLoading = useStoreValue(isLoadingStore);
 
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
 
    try {
      await login(email, password);
      // Redirect or show success message
    } catch (error) {
      // Error is already set in the store
      console.error("Login failed:", error);
    }
  };
 
  return (
    <div className="login-form">
      <h2>Login</h2>
 
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="email">Email</label>
          <input
            id="email"
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
            disabled={isLoading}
          />
        </div>
 
        <div className="form-group">
          <label htmlFor="password">Password</label>
          <input
            id="password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
            disabled={isLoading}
          />
        </div>
 
        {error && (
          <div className="error-message">
            {error}
          </div>
        )}
 
        <button type="submit" disabled={isLoading}>
          {isLoading ? "Logging in..." : "Login"}
        </button>
      </form>
    </div>
  );
}
// components/UserProfile.tsx
import { useStoreValue } from "@simplestack/store/react";
import { userStore, logout } from "../stores/authStore";
 
export function UserProfile() {
  const user = useStoreValue(userStore);
 
  if (!user) return null;
 
  return (
    <div className="user-profile">
      <div className="user-avatar">
        {user.avatar ? (
          <img src={user.avatar} alt={user.name} />
        ) : (
          <div className="avatar-placeholder">
            {user.name.charAt(0).toUpperCase()}
          </div>
        )}
      </div>
 
      <div className="user-info">
        <h3>{user.name}</h3>
        <p>{user.email}</p>
        <span className="user-role">{user.role}</span>
      </div>
 
      <button onClick={logout} className="btn-logout">
        Logout
      </button>
    </div>
  );
}
// components/ProtectedRoute.tsx
import { useStoreValue } from "@simplestack/store/react";
import { isAuthenticatedStore } from "../stores/authStore";
import { LoginForm } from "./LoginForm";
 
type ProtectedRouteProps = {
  children: React.ReactNode;
  requireAdmin?: boolean;
};
 
export function ProtectedRoute({ children, requireAdmin = false }: ProtectedRouteProps) {
  const isAuthenticated = useStoreValue(isAuthenticatedStore);
  const user = useStoreValue(userStore);
 
  if (!isAuthenticated) {
    return <LoginForm />;
  }
 
  if (requireAdmin && user?.role !== "admin") {
    return (
      <div className="access-denied">
        <h2>Access Denied</h2>
        <p>You don't have permission to view this page.</p>
      </div>
    );
  }
 
  return <>{children}</>;
}
// components/Header.tsx
import { useStoreValue } from "@simplestack/store/react";
import { isAuthenticatedStore } from "../stores/authStore";
import { UserProfile } from "./UserProfile";
import { CartBadge } from "./CartBadge";
 
export function Header() {
  const isAuthenticated = useStoreValue(isAuthenticatedStore);
 
  return (
    <header className="app-header">
      <div className="logo">My Store</div>
 
      <nav>
        <a href="/">Home</a>
        <a href="/products">Products</a>
        {isAuthenticated && <a href="/orders">Orders</a>}
      </nav>
 
      <div className="header-actions">
        <div className="cart-icon">
          🛒
          <CartBadge />
        </div>
 
        {isAuthenticated ? (
          <UserProfile />
        ) : (
          <a href="/login">Login</a>
        )}
      </div>
    </header>
  );
}

Step 4: Wire Everything Together

// App.tsx
import { AuthProvider } from "./components/AuthProvider";
import { Header } from "./components/Header";
import { ProtectedRoute } from "./components/ProtectedRoute";
 
function App() {
  return (
    <AuthProvider>
      <div className="app">
        <Header />
 
        <main>
          <ProtectedRoute>
            {/* Your protected content */}
            <h1>Welcome to your dashboard</h1>
          </ProtectedRoute>
        </main>
      </div>
    </AuthProvider>
  );
}
 
export default App;

Advanced Patterns

Pattern 1: Optimistic Updates with Queues

When dealing with async operations that need to happen in order (like file operations), use a queue:

import PQueue from "p-queue";
 
const queue = new PQueue({ concurrency: 1 });
 
export const addNote = (note: Note) => {
  const notesArray = notesStore.get();
 
  // Update UI immediately (optimistic)
  notesStore.set([...notesArray, note]);
 
  // Queue the async operation
  queue.add(async () => {
    try {
      await saveToDatabase(note);
    } catch (error) {
      // Revert on error
      notesStore.set(notesArray);
      throw error;
    }
  });
};

Pattern 2: Computed Values

Create derived stores for computed values:

// Create a computed store for cart item count
export const cartItemCountStore = store(0);
 
// Subscribe to cart changes and update count
cartItemsStore.subscribe((items) => {
  const count = items.reduce((sum, item) => sum + item.quantity, 0);
  cartItemCountStore.set(count);
});

Pattern 3: Persistence

Sync your store with localStorage:

// Load initial state from localStorage
const initialState = JSON.parse(
  localStorage.getItem("cart") ?? '{"items": [], "total": 0}'
);
 
export const cartStore = store<CartState>(initialState);
 
// Persist changes
cartStore.subscribe((state) => {
  localStorage.setItem("cart", JSON.stringify(state));
});

Pattern 4: DevTools Integration

Add logging for debugging:

if (import.meta.env.DEV) {
  authStore.subscribe((state) => {
    console.log("Auth state changed:", state);
  });
}

Performance Tips

1. Use Granular Selectors

// ❌ Bad - entire store subscription
function UserName() {
  const { user } = useStoreValue(authStore);
  return <span>{user?.name}</span>;
}
 
// ✅ Good - only subscribes to user
function UserName() {
  const user = useStoreValue(userStore);
  return <span>{user?.name}</span>;
}

2. Split Large Stores

// Instead of one large app store
const appStore = store({ auth: {}, cart: {}, theme: {}, ... });
 
// Create separate stores
const authStore = store({ ... });
const cartStore = store({ ... });
const themeStore = store({ ... });

3. Avoid Unnecessary Computations

// Compute derived values outside of renders
const cartTotal = cartItemsStore.get().reduce(...);
cartTotalStore.set(cartTotal);

Migration Guide

From Zustand

// Before (Zustand)
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));
 
// After (Simple Store)
const countStore = store(0);
const increment = () => countStore.set(countStore.get() + 1);

From Jotai

// Before (Jotai)
const countAtom = atom(0);
const [count, setCount] = useAtom(countAtom);
 
// After (Simple Store)
const countStore = store(0);
const count = useStoreValue(countStore);
// Set anywhere: countStore.set(newValue)

From Redux

// Before (Redux) - actions, reducers, selectors...
// After (Simple Store) - just create and use
const store = store({ count: 0 });

Common Pitfalls to Avoid

1. Not Using Selectors

Don't subscribe to the entire store when you only need part of it.

2. Creating Stores Inside Components

Always create stores outside of components for proper singleton behavior.

3. Forgetting Error Handling

Always handle errors in async operations and revert optimistic updates when needed.

Conclusion

Simple Store brings together the best ideas from the React state management ecosystem into a single, elegant solution. With just 220 lines of code, it provides:

  • The simplicity of Jotai's atoms
  • The granular control of Zustand's selectors
  • The directness of signals from SolidJS
  • Zero boilerplate like Redux demands

Whether you're building a shopping cart, managing authentication, or handling complex application state, Simple Store gives you the tools to do it cleanly and efficiently.

Quick Start Checklist

✅ Install: npm install @simplestack/store
✅ Create a store: const store = store(initialValue)
✅ Use in React: const value = useStoreValue(store)
✅ Select parts: store.select("key")
✅ Update: store.set(newValue)

Resources

Ready to simplify your state management? Give Simple Store a try and experience the joy of actually simple React state management.


Have questions or want to share your Simple Store implementation? Drop a comment below or open an issue on GitHub. Happy coding! 🚀