agent-rules-kit
Version:
Bootstrap of **Cursor** rules (`.mdc`) and mirror documentation (`.md`) for AI agent-guided projects.
761 lines (604 loc) • 16.8 kB
Markdown
MobX is a state management library that makes managing application state simple and scalable by applying object-oriented programming principles and reactive programming.
1. **Observable State**: Data that can be tracked for changes
2. **Actions**: Methods that modify observable state
3. **Computed Values**: Values derived from observable state that auto-update
4. **Reactions**: Side-effects that execute when observed data changes
5. **Stores**: Classes/objects that contain state and logic
```
src/
├── stores/
│ ├── index.js
│ ├── RootStore.js
│ ├── UserStore.js
│ └── ProductStore.js
├── hooks/
│ └── useStores.js
├── contexts/
│ └── StoreContext.js
└── components/
├── UserProfile.jsx
├── ProductList.jsx
└── ShoppingCart.jsx
```
First, install the required packages:
```bash
npm install mobx mobx-react-lite
yarn add mobx mobx-react-lite
```
```javascript
// stores/UserStore.js
import { makeAutoObservable, runInAction } from 'mobx';
import authService from '../services/authService';
class UserStore {
user = null;
isAuthenticated = false;
isLoading = false;
error = null;
constructor(rootStore) {
// Optional: Keep reference to root store for cross-store interactions
this.rootStore = rootStore;
// Make all properties observable, actions auto-bound, and enable computeds
makeAutoObservable(this);
// Check for saved authentication
this.checkAuth();
}
// Actions to modify state
setUser(user) {
this.user = user;
this.isAuthenticated = !!user;
}
setLoading(state) {
this.isLoading = state;
}
setError(error) {
this.error = error;
}
resetError() {
this.error = null;
}
// Async actions
async login(credentials) {
this.setLoading(true);
this.resetError();
try {
const user = await authService.login(credentials);
// Use runInAction for batch updates after async operation
runInAction(() => {
this.setUser(user);
this.setLoading(false);
});
return user;
} catch (error) {
runInAction(() => {
this.setError(error.message);
this.setLoading(false);
});
throw error;
}
}
async logout() {
this.setLoading(true);
try {
await authService.logout();
runInAction(() => {
this.setUser(null);
this.setLoading(false);
});
} catch (error) {
runInAction(() => {
this.setError(error.message);
this.setLoading(false);
});
throw error;
}
}
async checkAuth() {
const savedUser = localStorage.getItem('user');
if (savedUser) {
try {
const user = JSON.parse(savedUser);
this.setUser(user);
} catch (e) {
localStorage.removeItem('user');
}
}
}
// Computed values
get firstName() {
return this.user ? this.user.firstName : '';
}
get lastName() {
return this.user ? this.user.lastName : '';
}
get fullName() {
return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';
}
}
export default UserStore;
```
```javascript
// stores/ProductStore.js
import { makeAutoObservable, runInAction } from 'mobx';
import productService from '../services/productService';
class ProductStore {
products = [];
selectedProduct = null;
isLoading = false;
error = null;
constructor(rootStore) {
this.rootStore = rootStore;
makeAutoObservable(this);
}
// Actions
setProducts(products) {
this.products = products;
}
setSelectedProduct(product) {
this.selectedProduct = product;
}
setLoading(state) {
this.isLoading = state;
}
setError(error) {
this.error = error;
}
resetError() {
this.error = null;
}
// Async actions
async fetchProducts() {
this.setLoading(true);
this.resetError();
try {
const products = await productService.getAll();
runInAction(() => {
this.setProducts(products);
this.setLoading(false);
});
return products;
} catch (error) {
runInAction(() => {
this.setError(error.message);
this.setLoading(false);
});
throw error;
}
}
async fetchProductById(id) {
this.setLoading(true);
this.resetError();
try {
const product = await productService.getById(id);
runInAction(() => {
this.setSelectedProduct(product);
this.setLoading(false);
});
return product;
} catch (error) {
runInAction(() => {
this.setError(error.message);
this.setLoading(false);
});
throw error;
}
}
// Computed values
get productCount() {
return this.products.length;
}
get totalValue() {
return this.products.reduce((sum, product) => sum + product.price, 0);
}
get productsByCategory() {
const categories = {};
this.products.forEach((product) => {
if (!categories[product.category]) {
categories[product.category] = [];
}
categories[product.category].push(product);
});
return categories;
}
}
export default ProductStore;
```
```javascript
// stores/RootStore.js
import UserStore from './UserStore';
import ProductStore from './ProductStore';
import CartStore from './CartStore';
class RootStore {
constructor() {
// Initialize all stores with the root store instance
this.userStore = new UserStore(this);
this.productStore = new ProductStore(this);
this.cartStore = new CartStore(this);
}
}
export default RootStore;
```
```javascript
// stores/index.js
import RootStore from './RootStore';
// Create a single root store instance
const rootStore = new RootStore();
// Export individual stores for convenience
export const userStore = rootStore.userStore;
export const productStore = rootStore.productStore;
export const cartStore = rootStore.cartStore;
// Export the root store to access all stores
export default rootStore;
```
```javascript
// contexts/StoreContext.js
import React, { createContext, useContext } from 'react';
import rootStore from '../stores';
// Create a context with the root store
const StoreContext = createContext(rootStore);
// Provider component
export const StoreProvider = ({ children }) => {
return (
<StoreContext.Provider value={rootStore}>
{children}
</StoreContext.Provider>
);
};
// Hook to use the store context
export const useStores = () => {
const context = useContext(StoreContext);
if (context === undefined) {
throw new Error('useStores must be used within a StoreProvider');
}
return context;
};
// Hooks for individual stores
export const useUserStore = () => {
const { userStore } = useStores();
return userStore;
};
export const useProductStore = () => {
const { productStore } = useStores();
return productStore;
};
export const useCartStore = () => {
const { cartStore } = useStores();
return cartStore;
};
```
```jsx
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { StoreProvider } from './contexts/StoreContext';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<StoreProvider>
<App />
</StoreProvider>
</React.StrictMode>
);
```
```jsx
// components/LoginForm.jsx
import React, { useState } from 'react';
import { observer } from 'mobx-react-lite';
import { useUserStore } from '../contexts/StoreContext';
const LoginForm = observer(() => {
const userStore = useUserStore();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
await userStore.login({ email, password });
// Redirect or show success message
} catch (error) {
// Error is handled by the store and displayed below
}
};
return (
<form onSubmit={handleSubmit}>
{userStore.error && (
<div className='error-message'>{userStore.error}</div>
)}
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
id='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
id='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type='submit' disabled={userStore.isLoading}>
{userStore.isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
);
});
export default LoginForm;
```
```jsx
// components/ProductList.jsx
import React, { useEffect } from 'react';
import { observer } from 'mobx-react-lite';
import { useProductStore } from '../contexts/StoreContext';
import { useCartStore } from '../contexts/StoreContext';
const ProductList = observer(() => {
const productStore = useProductStore();
const cartStore = useCartStore();
useEffect(() => {
// Load products when component mounts
productStore.fetchProducts();
}, [productStore]);
if (productStore.isLoading) {
return <div>Loading products...</div>;
}
if (productStore.error) {
return <div>Error: {productStore.error}</div>;
}
return (
<div className='product-list'>
<h2>Products ({productStore.productCount})</h2>
{productStore.products.length === 0 ? (
<p>No products available.</p>
) : (
<ul>
{productStore.products.map((product) => (
<li key={product.id} className='product-item'>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button
onClick={() => cartStore.addToCart(product)}
disabled={cartStore.isItemInCart(product.id)}>
{cartStore.isItemInCart(product.id)
? 'In Cart'
: 'Add to Cart'}
</button>
</li>
))}
</ul>
)}
</div>
);
});
export default ProductList;
```
```javascript
// Example reaction in a component
import { reaction } from 'mobx';
import { useEffect } from 'react';
import { useCartStore } from '../contexts/StoreContext';
const CartNotification = () => {
const cartStore = useCartStore();
useEffect(() => {
// Set up a reaction that triggers when cart items change
const disposeReaction = reaction(
// Track the cart items length
() => cartStore.items.length,
// React to the changes
(length, previousLength) => {
if (length > previousLength) {
showNotification('Item added to cart');
} else if (length < previousLength) {
showNotification('Item removed from cart');
}
}
);
// Clean up the reaction when component unmounts
return () => {
disposeReaction();
};
}, [cartStore]);
// Component rendering
return null; // This component doesn't render anything visible
};
function showNotification(message) {
// Implementation of notification system
console.log(message);
}
```
```jsx
import React from 'react';
import { observer } from 'mobx-react-lite';
import { useUserStore, useCartStore } from '../contexts/StoreContext';
const Header = observer(() => {
const userStore = useUserStore();
const cartStore = useCartStore();
return (
<header className="app-header">
<div className="logo">My Store</div>
<div className="user-section">
{userStore.isAuthenticated ? (
<>
<span>Welcome, {userStore.fullName}</span>
<button onClick={() => userStore.logout()}>Logout</button>
</>
) : (
<button onClick={() => /* navigate to login */}>Login</button>
)}
</div>
<div className="cart-section">
<button>
Cart ({cartStore.itemCount}) - ${cartStore.totalPrice}
</button>
</div>
</header>
);
});
export default Header;
```
```jsx
import React from 'react';
import { observer } from 'mobx-react-lite';
import { makeAutoObservable } from 'mobx';
// Local component store
class FormState {
firstName = '';
lastName = '';
email = '';
errors = {};
constructor() {
makeAutoObservable(this);
}
setField(field, value) {
this[field] = value;
// Clear error when field is edited
if (this.errors[field]) {
delete this.errors[field];
}
}
validate() {
const errors = {};
if (!this.firstName) errors.firstName = 'First name is required';
if (!this.lastName) errors.lastName = 'Last name is required';
if (!this.email) {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(this.email)) {
errors.email = 'Email is invalid';
}
this.errors = errors;
return Object.keys(errors).length === 0;
}
get isValid() {
return Object.keys(this.errors).length === 0;
}
get formData() {
return {
firstName: this.firstName,
lastName: this.lastName,
email: this.email,
};
}
}
const RegistrationForm = observer(() => {
// Create local store for form
const [formState] = React.useState(() => new FormState());
const handleSubmit = (e) => {
e.preventDefault();
if (formState.validate()) {
console.log('Form submitted:', formState.formData);
// Submit to server or global store
}
};
return (
<form onSubmit={handleSubmit}>
<div className='form-group'>
<label htmlFor='firstName'>First Name</label>
<input
id='firstName'
value={formState.firstName}
onChange={(e) =>
formState.setField('firstName', e.target.value)
}
/>
{formState.errors.firstName && (
<div className='error'>{formState.errors.firstName}</div>
)}
</div>
<div className='form-group'>
<label htmlFor='lastName'>Last Name</label>
<input
id='lastName'
value={formState.lastName}
onChange={(e) =>
formState.setField('lastName', e.target.value)
}
/>
{formState.errors.lastName && (
<div className='error'>{formState.errors.lastName}</div>
)}
</div>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
id='email'
type='email'
value={formState.email}
onChange={(e) =>
formState.setField('email', e.target.value)
}
/>
{formState.errors.email && (
<div className='error'>{formState.errors.email}</div>
)}
</div>
<button type='submit' disabled={!formState.isValid}>
Register
</button>
</form>
);
});
export default RegistrationForm;
```
1. **Stores Structure**
- Create separate stores for different domains
- Use a root store to provide reference between stores
- Compose stores based on functionality
2. **State Mutation**
- Only modify state inside actions
- Use runInAction for updates after async operations
- Keep derivations and side effects separate from state changes
3. **Computed Properties**
- Use computed values for derived state
- Keep computeds pure and focused on a single responsibility
- Memoize complex calculations with computed properties
4. **Typescript Integration**
- Define interfaces for store state
- Type your actions and computed properties
- Use strict mode to catch errors early
5. **Performance Considerations**
- Use @observer on the lowest possible component level
- Split complex components into smaller observed parts
- Use @computed to memoize expensive calculations
6. **Testing**
- Test stores in isolation
- Mock dependencies (API services, etc.)
- Test reactions and side effects
## MobX vs. Redux
### When to Choose MobX
MobX is often a better choice when:
- You prefer a more object-oriented approach
- You want simpler, less boilerplate code
- Your state is complex and deeply nested
- You want a more flexible, less opinionated structure
- You're building a smaller to medium-sized application
- You want to iterate quickly with less setup
### When to Choose Redux
Redux might be better when:
- You want predictable state management with a strict unidirectional data flow
- You need time-travel debugging or state snapshots
- You prefer explicit, traceable state changes
- Your team is more familiar with functional programming concepts
- You're building a very large application with complex state changes
- You want more established patterns and guidelines