# Performance Optimization Standards for MobX
This document outlines coding standards specifically focused on performance optimization when using MobX. Following these guidelines will improve application speed, responsiveness, and resource usage. These standards are based on the latest MobX version and aim to promote maintainable and efficient codebases.
## 1. Computed Values: The Cornerstone of Efficient Derivation
Computed values are derived values that automatically update when their dependencies change. Efficient use of computed values is crucial for optimal MobX performance.
### 1.1. Standard: Use Computed Values for Derived Data
**Do This:**
"""typescript
import { makeObservable, observable, computed } from "mobx";
class Order {
@observable price: number = 0;
@observable quantity: number = 1;
constructor() {
makeObservable(this);
}
@computed get total(): number {
console.log("Calculating total"); // Demonstrates caching
return this.price * this.quantity;
}
}
const order = new Order();
console.log(order.total); // Output: Calculating total, (total)
order.price = 10;
console.log(order.total); // Output: Calculating total, 10
order.price = 10;
console.log(order.total); // Output: 10 (cached value)
"""
**Don't Do This:**
Calculating derived data directly within components or actions every time they are needed. This leads to unnecessary recalculations and performance bottlenecks.
"""typescript
import { observer } from "mobx-react-lite";
import { useState } from "react";
const MyComponent = observer(() => {
const [price, setPrice] = useState(0);
const [quantity, setQuantity] = useState(1);
const total = price * quantity; // Inefficient! Recalculated on every render.
return (
<p>Total: {total}</p>
{/* ... */}
);
});
"""
**Why:** Computed values are cached. MobX only re-evaluates them when their dependencies (observables they use) change. This avoids redundant calculations and significantly improves performance, especially with complex computations. The "console.log" in the getter demonstrates this caching behavior.
**Anti-Pattern:** Overusing computed values for simple data transformations that could be done efficiently within the component during rendering. Finding the right balance is necessary.
### 1.2. Standard: Memoize Expensive Computations Within Computed Values
**Do This:**
"""typescript
import { makeObservable, observable, computed } from "mobx";
import memoize from "lodash.memoize"; // Or your preferred memoization library
class DataProcessor {
@observable rawData: any[] = [];
constructor() {
makeObservable(this);
}
expensiveCalculation = (data: any[]) => {
console.log("Performing expensive calculation");
// Simulate a heavy computation (e.g., complex data aggregation)
return data.map((item) => item * 2).reduce((a, b) => a + b, 0);
};
@computed get processedData() {
return memoize(this.expensiveCalculation)(this.rawData);
}
}
const processor = new DataProcessor();
processor.rawData = [1, 2, 3, 4, 5];
console.log(processor.processedData); // Output: Performing expensive calculation, (result)
console.log(processor.processedData); // Output: (result) - memoized!
processor.rawData = [1, 2, 3, 4, 5]; // Same data, memoization kicks in.
console.log(processor.processedData); // Output: (result) - memoized again by lodash
processor .rawData = [1, 2, 3, 4, 6]; // Different Data
console.log(processor.processedData); //Output: Performing expensive calculation, (result)
"""
**Don't Do This:**
Performing expensive computations directly within the computed value without memoization.
**Why:** Memoization ensures that the expensive calculation is only performed when the input data ("this.rawData" in this case) actually changes. Using "lodash.memoize" (or similar) provides a simple way to cache the results of the calculation. Without memoization, the calculation would be re-run every time the "processedData" computed value is accessed, even if the underlying data hasn't changed.
**Note:** MobX 6+ has its own built in caching mechanics so this may not be needed.
### 1.3 Standard: Limit Scope of Computed Values
**Do This:**
Smaller, more granular computed values are generally more efficient because they are less likely to be invalidated unnecessarily. Break down large computed values into smaller, more manageable parts.
"""typescript
import { makeObservable, observable, computed } from "mobx";
class User {
@observable firstName: string;
@observable lastName: string;
constructor(firstName: string, lastName: string) {
makeObservable(this);
this.firstName = firstName;
this.lastName = lastName;
}
@computed get fullName(): string {
return "${this.firstName} ${this.lastName}";
}
}
class Address {
@observable street: string;
@observable city: string;
constructor(street: string, city: string) {
makeObservable(this);
this.street = street;
this.city = city;
}
@computed get fullAddress(): string {
return "${this.street}, ${this.city}";
}
}
class UserProfile {
user: User;
address: Address;
constructor(user: User, address: Address) {
this.user = user;
this.address = address;
}
}
"""
**Don't Do This:**
Creating a single, giant computed value that depends on many observables. Changes to any of those observables will trigger a re-evaluation of the entire computed value, even if the specific data that changed isn't relevant to the final result.
**Why:** Granular computed values enable MobX to perform more precise updates, re-evaluating only the computed values that are truly affected by the changes. This leads to fewer unnecessary computations and improved performance.
## 2. Reactivity Control: Optimizing Component Updates
MobX's reactivity system automatically updates components when relevant data changes. However, uncontrolled reactivity can lead to performance issues.
### 2.1. Standard: Use "observer" Wisely from "mobx-react-lite"
**Do This:**
Use "observer" on functional React components from the "mobx-react-lite" package. This provides optimized reactivity for functional components and avoids unnecessary re-renders.
"""typescript
import { observer } from "mobx-react-lite";
import { useStore } from "./storeContext"; // Assume a context provides access to the MobX store
const MyComponent = observer(() => {
const { data } = useStore(); // Access observable data from the store
return (
{data.map((item) => (
{item.name}
))}
);
});
export default MyComponent;
"""
**Don't Do This:**
Wrapping large, complex components with "observer" if only a small part of the component depends on MobX observables. This can cause re-renders even when the relevant data hasn't changed.
**Why:** "mobx-react-lite"'s "observer" is optimized for functional components and uses React context and hooks effectively. It ensures that components only re-render when the specific observables they use have changed. This avoids unnecessary re-renders, improving performance.
**Anti-Pattern:** Using the older "mobx-react" package with class components for new projects. "mobx-react-lite" is generally preferred for its performance characteristics, smaller size, and better integration with React Hooks.
### 2.2. Standard: Use "useMemo" for Non-Reactive Props
**Do This:**
If a component receives props that are not MobX observables, wrap the parts of the component that utilize these props with "useMemo".
"""typescript
import { observer } from "mobx-react-lite";
import React, { useMemo } from "react";
interface Props {
nonReactiveProp: string;
data: any[]; // This is assumed to be an observable
}
const MyComponent: React.FC = observer(({ nonReactiveProp, data }) => {
const memoizedContent = useMemo(() => {
console.log("Re-rendering memoized content");
return (
{nonReactiveProp}
);
}, [nonReactiveProp]);
return (
{memoizedContent}
{data.map(item => {item.name})}
);
});
"""
**Don't Do This:**
Relying solely on "observer" to prevent re-renders when non-reactive props change. "observer" only tracks changes to MobX observables.
**Why:** "useMemo" memoizes the result of the function it wraps, only re-running it when the dependencies in the dependency array change (in this case, "nonReactiveProp"). This ensures that the memoized content only re-renders when "nonReactiveProp" changes, even if the component itself re-renders due to changes in the MobX observables ("data" in this example).
### 2.3. Standard: Selecting specific data from observables for component use.
**Do This:**
Select *only* the data a component needs from the store using computed values or destructuring. This minimizes the scope of reactivity and reduces unnecessary re-renders.
"""typescript
import { observer } from "mobx-react-lite";
import { useStore } from "./storeContext";
import { computed } from "mobx";
const MyComponent = observer(() => {
const { userStore } = useStore();
// Computed value that only derives the username.
const username = computed(() => userStore.username).get();
return (
Welcome, {username}!
);
});
export default MyComponent;
"""
**Don't Do This:**
Passing the entire MobX store or large observable objects as props to components. This makes the component highly sensitive to changes throughout the store, leading to frequent re-renders.
**Why:** By selecting only the username, this component *only* re-renders if "userStore.username" changes.
**Anti-Pattern:** The alternative, and more common approach, looks like this:
"""typescript
import { observer } from "mobx-react-lite";
import { useStore } from "./storeContext";
const MyComponent = observer(() => {
const { userStore } = useStore();
return (
Welcome, {userStore.username}!
);
});
export default MyComponent;
"""
Although technically correct, that code directly subscribes the "MyComponent" Observer to the entire "userStore" making its render function more sensitive to store changes.
## 3. Actions: Streamlining State Mutations
Actions are the recommended way to modify MobX observables. Optimizing action performance is crucial for maintaining a responsive application.
### 3.1. Standard: Use "runInAction" for Synchronous Updates
**Do This:**
Use "runInAction" to group multiple synchronous observable mutations into a single transaction.
"""typescript
import { runInAction, observable } from "mobx";
class Store {
@observable count: number = 0;
increment() {
runInAction(() => {
this.count++;
// other state mutations
});
}
}
"""
**Don't Do This:**
Modifying observables directly outside of actions or running multiple actions for a single logical operation.
**Why:** "runInAction" batches updates, preventing unnecessary intermediate re-renders. This significantly improves performance when making multiple related state changes. It also provides semantic clarity, indicating that the code within the function is a state modification.
### 3.2. Standard: Debounce or Throttle Actions for High-Frequency Events
**Do This:**
Use "debounce" or "throttle" (from libraries like Lodash) for actions that are triggered frequently, such as input changes or scroll events.
"""typescript
import { observable, action } from "mobx";
import debounce from "lodash.debounce";
class SearchStore {
@observable searchTerm: string = "";
constructor() {
this.setSearchTermDebounced = debounce(this.setSearchTerm, 300);
}
@action setSearchTerm = (term: string) => {
this.searchTerm = term;
// Perform search logic here
};
setSearchTermDebounced: (term: string) => void; // Debounced version
}
const searchStore = new SearchStore();
//Component using the debounced search term
const SearchInput = ({searchStore}:any) => {
const handleChange = (event:any) => {
searchStore.setSearchTermDebounced(event.target.value)
}
return(
)
}
"""
**Don't Do This:**
Directly updating observables on every input change or scroll event. This can lead to excessive re-renders and degraded performance..
**Why:** Debouncing and throttling limit the frequency with which an action is executed. Debouncing ensures that the action is only executed after a certain period of inactivity, while throttling ensures that the action is executed at most once within a specified time frame. This reduces the number of state updates and re-renders, improving performance.
### 3.3. Standard: Asynchronous Actions with "flow" or "async/await" + "runInAction"
**Do This:**
For asynchronous operations that modify observables, use "flow" (from "mobx-utils") or "async/await" in combination with "runInAction".
"""typescript
import { observable, runInAction } from "mobx";
class DataStore {
@observable data: any[] = [];
@observable loading: boolean = false;
async fetchData() {
runInAction(() => {
this.loading = true;
});
try {
const response = await fetch("https://api.example.com/data");
const newData = await response.json();
runInAction(() => {
this.data = newData;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.loading = false;
console.error("Error fetching data:", error);
});
}
}
}
"""
**Don't Do This:**
Updating observables directly within asynchronous callbacks without using "runInAction".
**Why:** "runInAction" ensures that all observable updates within the asynchronous operation are batched into a single transaction. This prevents intermediate re-renders and ensures that the UI updates consistently. It's crucial for maintaining data consistency and preventing race conditions in asynchronous scenarios.
**Note:** "flow" from "mobx-utils" provides a more structured way to handle asynchronous actions, especially with complex logic. Consider using it for more advanced scenarios.
## 4. Rendering Optimizations
### 4.1. Standard: Use React.memo for Functional Components
**Do This:**
Wrap pure functional components (components that render the same output given the same props) with "React.memo". This prevents unnecessary re-renders when the props haven't changed. Combining "React.memo" with "observer" can lead to significant performance gains
"""typescript
import React from 'react';
interface Props {
name: string;
age: number;
}
const MyComponent: React.FC = React.memo(({ name, age }) => {
console.log("Rendering MyComponent with name: ${name}, age: ${age}");
return (
<p>Name: {name}</p>
<p>Age: {age}</p>
);
});
export default MyComponent;
"""
**Don't Do This:**
Relying solely on "observer" to optimize rendering of pure functional components. "observer" tracks changes to MobX observables, but "React.memo" prevents re-renders when the props themselves haven't changed.
**Why:** "React.memo" memoizes the component, preventing re-renders if the props are the same as the previous render. This is especially useful for components that receive complex data structures as props, as shallow comparison of objects and arrays can be expensive. Consider using a custom comparison function for "React.memo" if your props contain complex objects or arrays.
"""typescript
import React from 'react';
import isEqual from 'lodash.isequal'; // or a similar deep comparison library
interface Props {
data: {
name: string;
age: number;
address: {
street: string;
city: string;
};
};
}
const MyComponent: React.FC = React.memo(({ data }) => {
console.log('Rendering MyComponent');
return (
<p>Name: {data.name}</p>
<p>Age: {data.age}</p>
<p>Address: {data.address.street}, {data.address.city}</p>
);
}, (prevProps, nextProps) => isEqual(prevProps.data, nextProps.data));
export default MyComponent;
"""
### 4.2. Standard: Use Keys Effectively in Lists
**Do This:**
Always provide unique and stable keys when rendering lists of items. The keys should be derived from the data itself (e.g., a unique ID) and should not be based on the index of the item in the array..
"""typescript
import { observer } from "mobx-react-lite";
import { useStore } from "./storeContext";
const ItemList = observer(() => {
const { items } = useStore();
return (
{items.map((item) => (
{item.name}
))}
);
});
export default ItemList;
"""
**Don't Do This:**
Using the index of the item in the array as the key or omitting keys altogether.
**Why:** Keys help React identify which items in a list have changed, been added, or been removed. Using stable and unique keys allows React to efficiently update the DOM, minimizing unnecessary re-renders and maintaining component state. Using the index as a key can lead to performance issues and incorrect component state when the list is modified (e.g., by inserting or deleting items).
## 5. Large Datasets and Collections
Handling large datasets efficiently with MobX requires specific strategies.
### 5.1. Standard: Virtualization for Large Lists
**Do This:**
Use virtualization libraries (e.g., "react-window", "react-virtualized") to render only the visible items in a large list.
"""typescript
import { observer } from "mobx-react-lite";
import { FixedSizeList as List } from "react-window";
import { useStore } from "./storeContext";
const ItemList = observer(() => {
const { items } = useStore();
const Row = ({ index, style }: any) => (
{items[index].name}
);
return (
{Row}
);
});
export default ItemList;
"""
**Don't Do This:**
Rendering all items in a large list at once. This can lead to significant performance issues, especially with complex components.
**Why:** Virtualization only renders the items that are currently visible in the viewport, significantly reducing the number of DOM nodes and improving rendering performance. This is crucial for handling lists with thousands or even millions of items.
### 5.2. Standard: Consider Immutable Data Structures
**Do This:**
For very large and complex data structures, consider using immutable data structures (e.g., with Immutable.js or Immer). Modify your data in an immutable fashion and then reassign the MobX @observable.
**Why:** While it's not always needed, Immutable data structures can work very well with MobX.
**Anti-Pattern:** Trying to force immutability if it doesn't help solve a real problem.
## 6. Debugging and Profiling
Profiling MobX applications is essential to identify performance bottlenecks.
### 6.1. Standard: Use MobX DevTools
**Do This:**
Install and use the MobX DevTools browser extension to inspect the reactivity graph, track observable updates, and identify performance issues. [https://mobx.js.org/debugging.html](https://mobx.js.org/debugging.html)
**Why:** The MobX DevTools provides real-time insights into the behavior of your MobX application.
**Anti-Pattern:** Ignoring the MobX DevTools and trying to debug performance issues blindly can be a huge waste of time.
### 6.2. Standard: Profile React Components
**Do This:**
Use the React Profiler to identify slow-rendering components. Combine this information with the MobX DevTools to pinpoint the cause of the performance bottlenecks.
**Why:** The React Profiler combined with MobX DevTools offers a complete toolkit to find performance bottlenecks, linking slow components with slow-updating values, computed or actions.
## 7. Lazy Initialization
### 7.1. Standard: Initialize observables and compute values only when necessary.
Delay instantiation or population of observable properties until the data is requested or required by the user interface. This can reduce initial load times and memory consumption if not all data is immediately needed.
"""typescript
import { makeObservable, observable, computed } from 'mobx';
class UserProfile {
private _details?: UserDetails;
@observable loading: boolean = false;
constructor() {
makeObservable(this);
}
@computed get details(): UserDetails | undefined {
if (!this._details && !this.loading) {
this.loadDetails(); // Load user details only when they are accessed for the first time
}
return this._details;
}
async loadDetails() {
this.loading = true;
try {
const data = await fetchUserDetails();
this._details = data; // Assign fetched details
} finally {
this.loading = false;
}
}
}
interface UserDetails {
name: string;
age: number;
}
"""
**Why**: By lazily loading the user details, you're skipping the heavy database pull operation on component initialization, which can greatly affect performance.
## 8. General Notes
* Keep MobX version up-to-date to benefit from bug fixes and performance improvements.
* Use TypeScript to improve code maintainability and enable static type checking.
* Write unit tests to ensure the correctness of your MobX code.
* Monitor application performance regularly and identify areas for optimization.
By adhering to these coding standards, you can build high-performance MobX applications that are maintainable, scalable, and provide a great user experience. This document is a living document and should be updated as new best practices and features are introduced in MobX.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# API Integration Standards for MobX This document outlines the coding standards and best practices for integrating APIs with MobX-based applications. It aims to provide developers with clear guidelines for connecting to backend services and external APIs, ensuring maintainability, performance, and security. These guidelines leverage the latest features of MobX and promote modern development patterns. ## I. Architecture and Design ### 1. Separation of Concerns **Standard:** Isolate API interaction logic from UI components and core domain logic. **Do This:** Create dedicated services or repositories to handle API calls. Expose observable data from these services. **Don't Do This:** Directly perform API calls within UI components or MobX stores. This tightly couples the UI to the API, making testing and maintenance difficult. **Why:** Improves testability, reusability, and maintainability by decoupling concerns. Changes to the API layer don't directly impact the UI or domain logic. **Example:** """typescript // api/userService.ts import { makeAutoObservable, runInAction } from 'mobx'; class UserService { users: any[] = []; loading: boolean = false; error: string | null = null; constructor() { makeAutoObservable(this); } async fetchUsers() { this.loading = true; this.error = null; try { const response = await fetch('/api/users'); // Example endpoint const data = await response.json(); runInAction(() => { this.users = data; this.loading = false; }); } catch (e: any) { runInAction(() => { this.error = e.message; this.loading = false; }); } } } export const userService = new UserService(); // store/userStore.ts import { makeAutoObservable } from 'mobx'; import { userService } from '../api/userService'; class UserStore { constructor() { makeAutoObservable(this); } get users() { return userService.users; } get loading() { return userService.loading; } get error() { return userService.error; } fetchUsers() { userService.fetchUsers(); } } export const userStore = new UserStore(); // components/UserList.tsx import React, { useEffect } from 'react'; import { observer } from 'mobx-react-lite'; import { userStore } from '../store/userStore'; const UserList = observer(() => { useEffect(() => { userStore.fetchUsers(); }, []); if(userStore.loading){ return <div>Loading...</div> } if(userStore.error){ return <div>Error: {userStore.error}</div> } return ( <ul> {userStore.users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }); export default UserList; """ ### 2. Data Transformation and Mapping **Standard:** Perform data transformation between the API response and the MobX store's data structure within the service/repository layer. **Do This:** Map API data to a format suitable for your MobX stores before updating the observable state. Use tools like "class-transformer" or custom mapping functions for complex transformations. **Don't Do This:** Directly store API payloads in MobX stores without transformation. This can expose backend data structures directly to the frontend, leading to tight coupling and potential security vulnerabilities. **Why:** Ensures a consistent data structure within the application, regardless of API changes. Centralizes data transformation logic, making it easier to maintain and update. **Example:** """typescript // api/productService.ts import { makeAutoObservable, runInAction } from 'mobx'; import { Product } from '../store/productStore'; interface ApiResponse { product_id: number; product_name: string; price_usd: number; } class ProductService { products: Product[] = []; loading: boolean = false; error: string | null = null; constructor() { makeAutoObservable(this); } async fetchProducts() { this.loading = true; this.error = null; try { const response = await fetch('/api/products'); // Example endpoint const data: ApiResponse[] = await response.json(); const transformedProducts = data.map(item => ({ id: item.product_id, name: item.product_name, price: item.price_usd })); runInAction(() => { this.products = transformedProducts; this.loading = false; }); } catch (e: any) { runInAction(() => { this.error = e.message; this.loading = false; }); } } } export const productService = new ProductService(); """ ### 3. Error Handling **Standard:** Implement robust error handling in the API service layer. **Do This:** Catch errors from API calls, handle them gracefully, and expose error states through observable properties. Consider using a centralized error logging mechanism. Implement retry mechanisms with exponential backoff for transient errors. **Don't Do This:** Allow errors to propagate directly to UI components without handling. This results in a poor user experience and can expose sensitive information. **Why:** Improves application stability and provides informative feedback to the user in case of API failures. Centralized error handling simplifies debugging and maintenance. **Example:** """typescript // api/authService.ts import { makeAutoObservable, runInAction } from 'mobx'; class AuthService { isLoggedIn: boolean = false; loginError: string | null = null; loading: boolean = false; constructor() { makeAutoObservable(this); } async login(credentials: any) { this.loginError = null; this.loading = true; try { const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify(credentials), headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Login failed'); } runInAction(()=>{ this.isLoggedIn = true; this.loading = false; }) } catch (error: any) { runInAction(()=>{ this.loginError = error.message; this.isLoggedIn = false; this.loading = false; }) console.error('Login error:', error); // Log the error to a centralized logging service } } } export const authService = new AuthService(); // components/Login.tsx import React, { useState } from 'react'; import { observer } from 'mobx-react-lite'; import { authService } from '../api/authService'; const Login = observer(() => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const handleLogin = async () => { await authService.login({ username, password }); }; return ( <div> <input type="text" placeholder="Username" value={username} onChange={(e) => setUsername(e.target.value)} /> <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} /> <button onClick={handleLogin} disabled={authService.loading}>Login</button> {authService.loginError && <div style={{ color: 'red' }}>{authService.loginError}</div>} </div> ); }); export default Login; """ ### 4. Data Caching **Standard:** Implement a caching strategy to reduce the number of API calls. **Do This:** Use in-memory caching or browser storage (e.g., "localStorage", "sessionStorage", "IndexedDB") to store frequently accessed data. Implement cache invalidation strategies based on data changes. Leverage HTTP caching headers when possible. Consider using libraries like "cache-manager". **Don't Do This:** Aggressively cache data without proper invalidation. This can lead to stale data being displayed to the user. **Why:** Improves application performance by reducing network latency. Reduces load on the backend API. **Example (In-memory caching):** """typescript // api/articleService.ts import { makeAutoObservable, runInAction } from 'mobx'; const cache = new Map(); class ArticleService { articles: any[] = []; loading: boolean = false; error: string | null = null; cacheDuration: number = 60000 // 1 minute constructor() { makeAutoObservable(this); } async fetchArticles() { this.loading = true; this.error = null; const now = Date.now(); const cachedData = cache.get('articles'); if(cachedData && now - cachedData.timestamp < this.cacheDuration) { runInAction(() => { this.articles = cachedData.data; this.loading = false; return; }); } try { const response = await fetch('/api/articles'); // Example endpoint const data = await response.json(); runInAction(() => { this.articles = data; this.loading = false; cache.set('articles', { data, timestamp: Date.now() }); }); } catch (e: any) { runInAction(() => { this.error = e.message; this.loading = false; }); } } invalidateCache() { cache.delete('articles'); } } export const articleService = new ArticleService(); """ ## II. Implementation Details ### 1. API Client Libraries **Standard:** Use a dedicated HTTP client library for API interactions. **Do This:** Use "fetch" or "axios" for making HTTP requests. Configure the client with appropriate headers (e.g., "Authorization", "Content-Type") and error handling. Create reusable functions or classes for common API operations. Use libraries like "ky" for a smaller, more modern "fetch" wrapper. **Don't Do This:** Directly use low-level XMLHttpRequest objects. This makes the code harder to read, test, and maintain. **Why:** Simplifies API interactions and provides a consistent interface for making HTTP requests; improves code readability and maintainability; "axios" provides features like interceptors and automatic JSON parsing. **Example:** """typescript // api/apiClient.ts import axios from 'axios'; const apiClient = axios.create({ baseURL: '/api', // Define the base URL timeout: 10000, // Set a timeout headers: { 'Content-Type': 'application/json' } }); apiClient.interceptors.request.use( (config) => { // Add authentication token to the header: const token = localStorage.getItem('authToken'); // Example retrieval from localStorage if (token) { config.headers.Authorization = "Bearer ${token}"; } return config; }, (error) => { return Promise.reject(error); } ); apiClient.interceptors.response.use( (response) => { return response; }, (error) => { // Handle error globally: console.error('API Error:', error); return Promise.reject(error); } ); export default apiClient; // api/todoService.ts import apiClient from './apiClient'; import { makeAutoObservable, runInAction } from 'mobx'; class TodoService { todos: any[] = []; loading: boolean = false; error: string | null = null; constructor() { makeAutoObservable(this); } async fetchTodos() { this.loading = true; this.error = null; try { const response = await apiClient.get('/todos'); runInAction(()=>{ this.todos = response.data; this.loading = false; }) } catch (e: any) { runInAction(()=>{ this.error = e.messsage this.loading = false; }) } } async createTodo(todoData: any) { try { const response = await apiClient.post('/todos', todoData); runInAction(()=>{ this.todos.push(response.data) }) } catch (e: any) { runInAction(()=>{ this.error = e.messsage this.loading = false; }) } } } export const todoService = new TodoService(); """ ### 2. Optimistic Updates **Standard:** Implement optimistic updates for improved user experience. **Do This:** Update the UI immediately after the user initiates an action, assuming the API call will succeed. Revert the update if the API call fails. **Don't Do This:** Wait for the API call to complete before updating the UI. This can make the application feel sluggish. **Why:** Provides a more responsive user interface by immediately reflecting user actions; hides network latency from the user. **Example:** """typescript // store/commentStore.ts import { makeAutoObservable, runInAction } from 'mobx'; import { commentService } from '../api/commentService'; class CommentStore { comments: any[] = []; loading: boolean = false; error: string | null = null; constructor() { makeAutoObservable(this); } async addComment(text: string, postId: number) { // Optimistically add the comment const tempId = Date.now(); const newComment = { id: tempId, text: text, postId: postId, temp: true // Mark as temporary }; runInAction(() => { this.comments.push(newComment); }); try { const response = await commentService.addComment(text, postId); runInAction(() => { // Replace temporary comment with the real one this.comments = this.comments.map(comment => comment.id === tempId ? response.data : comment ); }); } catch (error: any) { runInAction(() => { // Revert the optimistic update this.comments = this.comments.filter(comment => comment.id !== tempId); this.error = error.message; }); console.error('Error adding comment:', error); } } } export const commentStore = new CommentStore(); // api/commentService.ts import apiClient from './apiClient'; import { makeAutoObservable, runInAction } from 'mobx'; class CommentService { constructor() { makeAutoObservable(this); } async addComment(text: string, postId: number) { const response = await apiClient.post('/comments', { text: text, postId: postId }); return response; } } export const commentService = new CommentService(); """ ### 3. Handling Loading States **Standard:** Represent loading states accurately using observable properties. **Do This:** Create boolean observable properties like "isLoading" in your stores to track the loading state of API calls. Update these properties before and after API calls. Disable UI elements or show loading indicators based on these properties. **Don't Do This:** Rely on implicit loading states or manipulate the DOM directly to show loading indicators. **Why:** Provides a consistent and reactive way to manage loading states in the UI; improves user experience by providing visual feedback during API calls; simplifies testing by making loading states observable. **Example (See other examples):** Refer to example in section I.1, I.3, II.1 ### 4. Debouncing and Throttling **Standard</strong>: Use debouncing and throttling techniques to prevent excessive API calls. **Do This:** When dealing with rapidly changing input, such as search queries or form inputs, use debouncing or throttling libraries like "lodash" or "rxjs" to limit the frequency of API calls. **Don't Do This:** Trigger an API call on every keystroke or input change. This can overwhelm the backend and degrade performance. **Why:** Reduces unnecessary API calls, improving application performance and reducing load on the backend. **Example (Debouncing with Lodash):** """typescript // components/Search.tsx import React, { useState, useCallback, useEffect } from 'react'; import { observer } from 'mobx-react-lite'; import { searchService } from '../api/searchService'; // Assuming you have a search service import { debounce } from 'lodash'; const SearchComponent = observer(() => { const [searchTerm, setSearchTerm] = useState(''); const [results, setResults] = useState([]); const handleSearch = async (term: string) => { const data = await searchService.search(term); setResults(data); }; // Debounce the search function const debouncedSearch = useCallback( debounce((term: string) => { handleSearch(term); }, 300), // Delay of 300ms [] ); const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const newSearchTerm = event.target.value; setSearchTerm(newSearchTerm); debouncedSearch(newSearchTerm); }; useEffect(() => { // Initial search when the component mounts, if needed if (searchTerm) { debouncedSearch(searchTerm); } // Cleanup the debounced function on unmount return () => { debouncedSearch.cancel(); }; }, [searchTerm, debouncedSearch]); return ( <input type="text" placeholder="Search..." value={searchTerm} onChange={handleChange} /> {results.map(result => ( {result.title} ))} ); }); export default SearchComponent; // api/searchService.ts import apiClient from './apiClient'; import { makeAutoObservable } from 'mobx'; class SearchService { loading: boolean = false; error: string | null = null; constructor() { makeAutoObservable(this); } async search(term: string) { this.loading = true; this.error = null; try { const response = await apiClient.get("/search?q=${term}"); this.loading = false; return response.data; } catch (error: any) { this.error = error.message this.loading = false; return []; } } } export const searchService = new SearchService(); """ ## III. Security Considerations ### 1. Data Validation **Standard:** Validate API request data on the client-side and server-side. **Do This:** Use libraries like "yup" or "joi" on the client-side to validate user input before sending it to the API. Implement server-side validation to prevent malicious data from being stored. **Don't Do This:** Rely solely on client-side validation. It can be bypassed by malicious users. **Why:** Prevents security vulnerabilities like SQL injection or cross-site scripting (XSS). Ensures data integrity and consistency. ### 2. Authentication and Authorization **Standard:** Secure API endpoints with proper authentication and authorization mechanisms. **Do This:** Use industry-standard authentication protocols like OAuth 2.0 or JWT for securing API endpoints. Store authentication tokens securely in the browser (e.g., using "httpOnly" cookies or the "Web Storage API"). Implement authorization checks on the backend to ensure that users only have access to the resources they are allowed to access. **Don't Do This:** Store sensitive information in plain text in the browser. Implement authentication or authorization logic on the client-side only. Bypass authentication/authorization checks on the server-side. **Why:** Protects sensitive data and prevents unauthorized access to API endpoints. Ensures that only authenticated and authorized users can perform specific actions. ### 3. CORS Configuration **Standard:** Configure Cross-Origin Resource Sharing (CORS) properly on the server-side. **Do This:** Configure CORS to allow requests from your application's domain(s). Use a whitelist of allowed origins instead of allowing all origins ("Access-Control-Allow-Origin: *"). Set appropriate "Access-Control-Allow-Methods" and "Access-Control-Allow-Headers" to restrict the allowed HTTP methods and headers. **Don't Do This:** Allow all origins in the CORS configuration ("Access-Control-Allow-Origin: *"). This can expose your API to security vulnerabilities. Forget to set proper "Access-Control-Allow-Methods" and "Access-Control-Allow-Headers". **Why:** Prevents cross-origin attacks by ensuring that only authorized domains can access your API. ### 4. Secrets Management **Standard:** Store API keys and other sensitive information securely. **Do This:** Use environment variables or dedicated secrets management tools to store API keys and other sensitive information. Never commit API keys directly to the source code repository. Protect the .env file and/or use more advanced methods. **Don't Do This:** Hardcode API keys directly in the source code. This can expose sensitive information if the code is compromised. **Why:** Prevents unauthorized access to your API and protects sensitive information. ## IV. Testing ### 1. Unit Tests **Standard:** Write unit tests for API service/repository classes. **Do This:** Mock API calls using libraries like "jest" or "Sinon.JS" to isolate the service/repository logic. Test different scenarios, including success, failure, and edge cases. **Don't Do This:** Skip unit tests for API service/repository classes. This makes it difficult to verify the correctness of the API integration logic. **Why:** Ensures that the API service/repository classes are functioning correctly and that the application is resilient to API changes. ### 2. Integration Tests **Standard:** Write integration tests to verify the interaction between the frontend and the backend API. **Do This:** Set up a test environment that mimics the production environment. Use tools like "Cypress" or "Selenium" to automate the integration tests. **Don't Do This:** Rely solely on manual testing for API integration. This is time-consuming and error-prone. **Why:** Verifies that the frontend and backend are working together correctly and that the API integration is functioning as expected. ## V. Monitoring and Logging ### 1. API Monitoring **Standard:** Monitor API performance and availability. **Do This:** Use tools to monitor API response times, error rates, and other key metrics. Set up alerts to notify you of potential issues. **Don't Do This:** Ignore API performance and availability. This can lead to a poor user experience and lost revenue. **Why:** Ensures that the API is performing as expected and that any issues are detected and resolved quickly. ### 2. API Logging **Standard:** Log API requests and responses. **Do This:** Log relevant information about API requests and responses, such as the request URL, method, headers, and body. Use a structured logging format like JSON to make it easier to analyze the logs. **Don't Do This:** Log sensitive information in plain text. Comply with privacy regulations and security best practices. **Why:** Provides valuable insights into API usage and can help diagnose issues. By following these coding standards, you can ensure that your MobX applications integrate with APIs efficiently, securely, and maintainably. This document should be treated as a living document, updated as new best practices and MobX features emerge.
# Core Architecture Standards for MobX This document outlines the core architectural standards for MobX-based applications, focusing on project structure, organization, and fundamental patterns to ensure maintainability, performance, and scalability. These standards are designed to be used as a reference for developers and as context for AI coding assistants. ## 1. Project Structure and Organization A well-defined project structure is crucial for the long-term maintainability of any application. Here are standard guidelines specific to MobX projects: ### 1.1 Modularization **Standard:** Organize your application into modular components or features. Each module should encapsulate a specific area of functionality and contain its own MobX stores, UI components, and related logic. **Why:** Modularity improves code reusability, testability, and maintainability by isolating concerns and reducing dependencies. **Do This:** Separate features into distinct directories. For example, an e-commerce application might have "src/products", "src/cart", and "src/user" directories. **Don't Do This:** Pile all MobX stores and components into a single "src/store" or "src/components" directory, creating a monolithic application. **Example:** """ src/ ├── products/ │ ├── ProductList.jsx // React Component │ ├── ProductStore.js // MobX Store │ ├── ProductApi.js // API interactions │ └── ... ├── cart/ │ ├── Cart.jsx // React Component │ ├── CartStore.js // MobX Store │ └── ... ├── user/ │ ├── UserProfile.jsx // React Component │ ├── UserStore.js // MobX Store │ └── ... └── app.jsx // Root component """ ### 1.2 Separation of Concerns **Standard:** Clearly separate your application's concerns: data management (MobX stores), UI components (React/Vue), API interactions, and business logic. **Why:** Separation of concerns improves code readability, testability, and allows for easier modification of one part of the application without affecting others. **Do This:** Keep UI components focused on rendering data from MobX stores. Abstract API calls and complex business logic into separate services or utility functions. **Don't Do This:** Embed API calls directly within React components or MobX store actions. Place complex business logic inside of UI components. **Example:** """javascript // ProductStore.js import { makeAutoObservable, runInAction } from "mobx"; import { fetchProducts } from "./ProductApi"; class ProductStore { products = []; loading = false; constructor() { makeAutoObservable(this); } async loadProducts() { this.loading = true; try { const products = await fetchProducts(); runInAction(() => { this.products = products; }); } finally { runInAction(() => { this.loading = false; }); } } } export default new ProductStore(); // ProductApi.js (Separates API interaction) export async function fetchProducts() { const response = await fetch('/api/products'); if (!response.ok) { throw new Error('Failed to fetch products'); } return await response.json(); } // ProductList.jsx (UI Component) import React, { useEffect } from 'react'; import { observer } from 'mobx-react-lite'; import productStore from './ProductStore'; const ProductList = observer(() => { useEffect(() => { productStore.loadProducts(); }, []); if (productStore.loading) { return <p>Loading...</p>; } return ( <ul> {productStore.products.map(product => ( <li key={product.id}>{product.name}</li> ))} </ul> ); }); export default ProductList; """ ### 1.3 Layered Architecture **Standard:** Implement a layered architecture consisting (typically) of UI, application/domain logic, and data layers. **Why:** Simplifies testing, enables independent scaling, and facilitates reuse. **Do This:** Use React components for UI, MobX stores for application/domain logic, and dedicated services/repositories for data handling. **Don't Do This:** Blend the UI layer with data handling, or write domain logic inside React components. ## 2. Architectural Patterns for MobX Applications Several architectural patterns enhance MobX application design. Here are the dominant patterns you must know. ### 2.1 Model-View-ViewModel (MVVM) **Standard:** Adopt the MVVM pattern, where MobX stores serve as the ViewModel, providing data to the View (React components). **Why:** MVVM promotes a clean separation between the UI (View) and the application logic (ViewModel), improving testability and maintainability. **Do This:** Design stores to expose the specific data needed by the corresponding views. Use computed values to format store data for ease of consumption by views. **Don't Do This:** Pass entire store instances to UI components and have the components directly manipulate the store's internal state. Stores should define specific actions to modify state. **Example:** """javascript // PersonStore.js (ViewModel) import { makeAutoObservable, computed } from "mobx"; class PersonStore { firstName = "John"; lastName = "Doe"; constructor() { makeAutoObservable(this); } get fullName() { return "${this.firstName} ${this.lastName}"; } setFirstName(firstName) { this.firstName = firstName; } } export default new PersonStore(); // PersonView.jsx (View) import React from 'react'; import { observer } from 'mobx-react-lite'; // or 'mobx-react' import personStore from './PersonStore'; const PersonView = observer(() => { return ( <div> <p>Full Name: {personStore.fullName}</p> <input value={personStore.firstName} onChange={(e) => personStore.setFirstName(e.target.value)} /> </div> ); }); export default PersonView; """ ### 2.2 Service Pattern **Standard:** Introduce a Service Layer (aka API Layer) to abstract all data fetching and API interactions. **Why:** Centralizes all API logic, allows mocking in tests, and enables switching between different data sources without modifying application stores or UI components. **Do This:** Create dedicated service classes or functions that handle API calls and transform the data into a format suitable for the stores. **Don't Do This:** Put API calls directly inside stores or React components. **Example:** """javascript // UserService.js (Service Layer) export async function fetchUser(userId) { const response = await fetch("/api/users/${userId}"); if (!response.ok) { throw new Error('Failed to fetch user'); } return await response.json(); } // UserStore.js import { makeAutoObservable, runInAction } from "mobx"; import { fetchUser } from "./UserService"; class UserStore { user = null; constructor() { makeAutoObservable(this); } async loadUser(userId) { try { const user = await fetchUser(userId); runInAction(() => { this.user = user; }); } catch (error) { // Handle error, update the store (e.g. with an error flag) console.error("Error loading user:", error) } } } export default new UserStore(); """ ### 2.3 Repository Pattern **Standard:** Use a Repository Pattern for managing data access, especially when dealing with persistent storage or complex database interactions. **Why:** It abstracts the data access logic and shields the ViewModel (MobX store) from the details of how data is stored and retrieved. This enhances testability and enables easier switching between different storage implementations (e.g. switching from local storage to a backend database). **Do This:** Create repository classes/functions to handle CRUD operations and any data transformations specific to the data source. **Don't Do This:** Directly interact with databases or other persistent storage mechanisms from within MobX stores. **Example:** """javascript // UserRepository.js (Repository pattern) class UserRepository { async getUser(userId) { // Logic to retrieve user from a database or other persistent storage // Example (using a hypothetical database client): const user = await db.query("SELECT * FROM users WHERE id = ?", userId); return user; } // Other CRUD operations (createUser, updateUser, deleteUser) go here } const userRepository = new UserRepository(); export default userRepository; // UserStore.js import { makeAutoObservable, runInAction } from "mobx"; import userRepository from "./UserRepository"; class UserStore { user = null; constructor() { makeAutoObservable(this); } async loadUser(userId) { try { const user = await userRepository.getUser(userId); runInAction(() => { this.user = user; }); } catch (error) { // Handle error } } } export default new UserStore(); """ ### 2.4 Action Orchestration **Standard:** Actions in stores should be single-purpose, reflecting user intent. For more complex operations, use "orchestration actions" to coordinate multiple simpler actions. **Why:** Keeps individual actions focused and testable, with orchestration actions providing a clear entry point for complex operations. **Do This:** Create small, focused actions to manipulate the store's data. Create a separate, larger "orchestration" action that combines multiple smaller actions to, e.g., complete a transaction. **Don't Do This:** Create actions that do many things at once. **Example:** """javascript // CartStore.js import { makeAutoObservable, runInAction } from "mobx"; class CartStore { items = []; constructor() { makeAutoObservable(this); } addItem(item) { this.items.push(item); } removeItem(itemId) { this.items = this.items.filter(item => item.id !== itemId); } clearCart() { this.items = []; } // Orchestration action async checkout() { try { // 1. Validate items (example) if (this.items.length === 0) { throw new Error("Cart is empty"); } // 2. Submit order (example - would call an API) // await submitOrder(this.items); // 3. Clear the cart (after successful submission) runInAction(() => { this.clearCart(); }); alert("Checkout successful!"); } catch (error) { alert("Checkout failed: " + error.message); } } } export default new CartStore(); // CartComponent.jsx import React from 'react'; import { observer } from 'mobx-react-lite'; import cartStore from './CartStore'; const CartComponent = observer(() => { return ( <div> <button onClick={() => cartStore.checkout()}>Checkout</button> {/* ... display cart items */} </div> ); }); export default CartComponent; """ ## 3. MobX Store Design Principles ### 3.1 Single Source of Truth **Standard:** Each piece of application state should live in only one MobX store. **Why:** Prevents data inconsistencies and simplifies debugging. If the same data is needed in multiple components, access a single data source instead of duplicating the data. **Do This:** Centralize state management within MobX stores. If multiple modules need access to the same data, consider creating a shared store or a composition pattern. **Don't Do This:** Duplicate state across multiple stores or components. ### 3.2 Data Normalization **Standard:** Normalize your data structures in stores. Avoid deeply nested objects; instead, use flat structures and IDs for relationships. **Why:** Simplifies data management, enables efficient updates, and improves performance. **Do This:** Use flat data structures with IDs to represent relationships, similar to relational database design. Use computed values to derive nested or formatted data when needed. **Don't Do This:** Store deeply nested objects directly in the store(s). **Example:** """javascript // Normalized data class AuthorStore { authors = { 1: { id: 1, name: "Jane Austen" }, 2: { id: 2, name: "Leo Tolstoy" } }; getAuthor(id) { return this.authors[id]; } constructor() { makeAutoObservable(this); } } class BookStore { books = { 101: { id: 101, title: "Pride and Prejudice", authorId: 1 }, 102: { id: 102, title: "War and Peace", authorId: 2 } }; constructor() { makeAutoObservable(this); } getBook(id) { return this.books[id]; } } const authorStore = new AuthorStore(); const bookStore = new BookStore() bookStore.getBook(101).author = authorStore.getAuthor(1); """ ### 3.3 Immutable Updates **Standard:** While MobX automatically tracks changes, it's good practice to treat data immutably when updating complex objects. **Why:** Can prevent unexpected side effects (especially when combined with some UI frameworks' rendering behaviors) and simplifies debugging. **Do This:** Use techniques like the spread operator or "immer" library to create new objects with updated values instead of directly mutating existing objects. **Don't Do This:** Directly modify properties of existing complex objects in the store. **Example:** """javascript // Using the spread operator for immutable updates import { makeAutoObservable, runInAction } from "mobx"; class TodoStore { todos = [{ id: 1, text: "Learn MobX", completed: false }]; constructor() { makeAutoObservable(this); } toggleTodo(id) { runInAction(() => { this.todos = this.todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ); }); } } export default new TodoStore(); // Immer Example import { makeAutoObservable, runInAction} from "mobx"; import { produce } from "immer" class BookStore { book = { title: "Some book", author: { firstName: "Some", lastName: "Guy" } } updateBook(bookChanges){ this.book = produce(this.book, (draft) => { Object.assign(draft, bookChanges); }) } constructor() { makeAutoObservable(this); } } """ ## 4. Scalability Considerations ### 4.1 Lazy Loading of Stores **Standard:** Load stores and their data only when they are needed, especially in large applications with many features. **Why:** Reduces initial load time and improves the overall performance of the application. **Do This:** Implement code splitting and lazy loading of modules containing MobX stores. Initialize stores only when the corresponding feature is accessed. **Don't Do This:** Initialize all stores upfront when the application starts. ### 4.2 Store Composition **Standard:** Create complex stores by composing simpler, more focused stores. **Why:** Promotes code reusability, testability, and maintainability. Allows you to break down complex application state into manageable units. **Do This:** Design smaller, independent stores and compose them into larger stores as needed. Inject dependencies between stores. **Don't Do This:** Create monolithic stores that handle multiple unrelated concerns. **Example:** """javascript // ThemeStore.js import { makeAutoObservable } from "mobx"; class ThemeStore { theme = 'light'; constructor() { makeAutoObservable(this); } toggleTheme() { this.theme = this.theme === 'light' ? 'dark' : 'light'; } } export default new ThemeStore(); // UserStore.js import { makeAutoObservable } from "mobx"; class UserStore { loggedIn = false; username = null; constructor() { makeAutoObservable(this); } login(username) { this.loggedIn = true; this.username = username; } logout() { this.loggedIn = false; this.username = null; } } export default new UserStore(); // AppStore.js (Composed Store) import themeStore from "./ThemeStore"; import userStore from "./UserStore"; class AppStore { themeStore; userStore; constructor() { this.themeStore = themeStore; this.userStore = userStore; } } export default new AppStore(); """ ### 4.3 Performance Profiling **Standard:** Regularly profile your MobX application to identify and address performance bottlenecks. **Why:** Ensures the application remains responsive and efficient as it grows in complexity. **Do This:** Use MobX devtools to monitor the performance of your stores. Identify hot spots where computations are taking too long or unnecessary re-renders are occurring. Use best practices to reduce the number of re-renders (e.g. use React.memo liberally). **Don't Do This:** Neglect performance monitoring, especially as the application grows larger. ## 5. Modernization and Latest MobX Features ### 5.1 makeAutoObservable vs. Annotations **Standard:** Prefer "makeAutoObservable" or "makeObservable" over legacy decorators. **Why:** "makeAutoObservable" typically requires less code and offers more concise and readable syntax. "makeObservable" allows for more granular control but is more verbose. **Do This:** Use "makeAutoObservable(this)" in your constructor for simple cases. Use "makeObservable(this, { ... })" when you need explicit control over observable properties, actions, and computed values. **Don't Do This:** Continue using class decorators ("@observable", "@action", "@computed") from older MobX versions as they are less maintainable, prone to issues with newer versions, and not as widely supported in modern tooling. ### 5.2 Observer Hook (mobx-react-lite + functional components) **Standard:** Utilize the "observer" hook in conjunction with functional React components. **Why:** Functional components are easier to test and reason, offering better ways to manage component state. "mobx-react-lite" is a lighter-weight, optimized version for React 16.8+. **Do This:** Wrap functional components with "observer()" to trigger re-renders when the observed data changes within the MobX stores. **Don't Do This:** Rely exclusively on class-based components with "@observer" (from legacy "mobx-react") when functional components offer a cleaner and more efficient approach. This core architecture document provided critical foundational guidance for consistent MobX projects. Future documents will focus on further areas.
# Tooling and Ecosystem Standards for MobX This document outlines the recommended tooling and ecosystem practices for MobX development. Adhering to these standards will improve code quality, maintainability, performance, and overall development experience. This rule focuses specifically on tools and libraries that enhance MobX development. ## 1. Development Environment Setup ### 1.1. IDE Configuration **Standard:** Configure your IDE for optimal MobX development. * **Do This:** * Use an IDE with JavaScript/TypeScript support (e.g., VS Code, WebStorm). * Install relevant extensions for syntax highlighting, linting, and debugging. * Configure code formatting tools like Prettier to ensure consistent code style. * **Don't Do This:** * Rely on basic text editors without proper JavaScript/TypeScript support. * Ignore IDE warnings and errors. * Use inconsistent code formatting. **Why:** A well-configured IDE significantly improves development speed and reduces errors by providing real-time feedback and code assistance. **Example (VS Code settings.json):** """json { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "javascript.validate.enable": true, "typescript.validate.enable": true, "eslint.enable": true, "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact" ] } """ ### 1.2. Linting and Static Analysis **Standard:** Utilize linters and static analysis tools to enforce code quality and prevent errors. * **Do This:** * Integrate ESLint with recommended MobX-specific rules (e.g., "eslint-plugin-mobx"). * Use TypeScript for static typing when appropriate. * Enable strict mode in TypeScript (""strict": true" in "tsconfig.json"). * **Don't Do This:** * Ignore linting warnings and errors. * Disable important linting rules without a valid reason. * Use JavaScript without static typing when possible. **Why:** Linting and static analysis catch potential bugs early in the development process and ensure code adheres to established standards. **Example (ESLint configuration .eslintrc.js):** """javascript module.exports = { parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint', 'mobx'], extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:mobx/recommended' ], rules: { 'mobx/observable-props-uses-decorators': 'warn' }, parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, env: { browser: true, node: true, }, }; """ ## 2. State Management and Data Flow ### 2.1. MobX DevTools **Standard:** Use MobX DevTools for inspecting and debugging MobX state. * **Do This:** * Install the MobX DevTools browser extension. * Enable MobX DevTools in your application (usually automatic when "mobx" is imported). * Use the DevTools to inspect observables, track computed values, and visualize reactions. * **Don't Do This:** * Ignore MobX DevTools during debugging. * Disable MobX DevTools in development environments. **Why:** MobX DevTools provide invaluable insights into the runtime behavior of your MobX application, making debugging and performance tuning much easier. **Example:** No code needed; install the extension and "import 'mobx'" in your application entry point. Consult the official MobX documentation for configuration options. ### 2.2. "mobx-react" or "mobx-react-lite" **Standard:** Use "mobx-react" or "mobx-react-lite" for efficient React integration. * **Do This:** * Wrap React components that use observables with "observer" from "mobx-react" or "mobx-react-lite". * Use "mobx-react-lite" for smaller bundle sizes and improved performance in modern React applications. * Leverage "useLocalObservable" or dependency injection instead of class-based components. * **Don't Do This:** * Manually trigger component updates when observables change. * Use "mobx-react" in projects where size and performance are paramount (consider "mobx-react-lite"). **Why:** "mobx-react" and "mobx-react-lite" automatically optimize React component updates based on observable dependencies, preventing unnecessary re-renders. **Example ("mobx-react-lite" with "useLocalObservable"):** """typescript import React from 'react'; import { useLocalObservable, observer } from 'mobx-react-lite'; const Counter = observer(() => { const store = useLocalObservable(() => ({ count: 0, increment() { this.count++; }, decrement() { this.count--; } })); return ( <div> Count: {store.count} <button onClick={store.increment}>Increment</button> <button onClick={store.decrement}>Decrement</button> </div> ); }); export default Counter; """ ### 2.3. Asynchronous Actions and State Updates **Standard:** Handle asynchronous actions and state updates correctly using "runInAction", "flow", or "makeAutoObservable" * **Do This:** * Wrap any code that modifies state directly inside a "runInAction" block. * Use "flow" for complex asynchronous operations. Async operations are often a great fit for wrapping in a generator function. * Use "makeAutoObservable" with asynchronous methods. * **Don't Do This:** * Modify state directly in asynchronous callbacks without "runInAction" (or related mechanisms like generators or makeAutoObservable capabilities.) * Mix and match different mechanisms (e.g., using callbacks within "flow" or "runInAction" without proper context). **Why:** Ensure that state updates are batched and tracked correctly, preventing race conditions and improving performance. This leads to cleaner and more maintainable code. **Example ("runInAction" with async/await):** """typescript import { makeObservable , observable, action, runInAction } from "mobx" class Todo { id = Math.random(); title; finished = false; constructor(title) { makeObservable(this, { title: observable, finished: observable, toggle: action }) this.title = title; } toggle() { this.finished = !this.finished; } } class TodoList { todos = []; pendingRequestCount = 0; constructor() { makeObservable(this, { todos: observable, pendingRequestCount: observable, loadTodos: action }) } async loadTodos() { this.pendingRequestCount++; try { const todos = [{title: "Learn MobX"},{title: "Write documentation"}]; // await fetchTodos(); // example fetching from external API runInAction(() => { todos.forEach(json => this.todos.push(new Todo(json.title))); }) } finally { runInAction(() => { this.pendingRequestCount-- }); } } } const observableTodoList = new TodoList(); observableTodoList.loadTodos(); """ **Example (flow):** """typescript import { flow, makeObservable, observable } from 'mobx'; class Store { data = null; loading = false; error = null; constructor() { makeObservable(this, { data: observable, loading: observable, error: observable, fetchData: flow }); } * fetchData(url) { this.loading = true; try { const response = yield fetch(url); this.data = yield response.json(); } catch (e) { this.error = e; } finally { this.loading = false; } } } // Usage const store = new Store(); store.fetchData('/api/data'); """ ### 2.4. Dependency Injection **Standard:** Utilize dependency injection to manage dependencies and improve testability. * **Do This:** * Use a dependency injection container (e.g., InversifyJS, Awilix) to manage dependencies. * Inject stores and services into components and other stores. * Use contexts (React.createContext) for accessing dependencies in React components. * **Don't Do This:** * Create store instances directly within components or other stores. * Rely on global variables for accessing dependencies. **Why:** Dependency injection promotes loose coupling, making code more modular, testable, and maintainable. **Example (React Context with "useContext"):** """typescript import React, { createContext, useContext } from 'react'; import { useLocalObservable, observer } from 'mobx-react-lite'; // Create a context for the store const StoreContext = createContext(null); // Provider component to wrap the application export const StoreProvider = ({ children }) => { const store = useLocalObservable(() => ({ count: 0, increment() { this.count++; }, decrement() { this.count--; } })); return ( <StoreContext.Provider value={store}> {children} </StoreContext.Provider> ); }; // Custom hook to access the store export const useStore = () => { const store = useContext(StoreContext); if (!store) { throw new Error('useStore must be used within a StoreProvider'); } return store; }; // Component using the store const Counter = observer(() => { const store = useStore(); return ( <div> Count: {store.count} <button onClick={store.increment}>Increment</button> <button onClick={store.decrement}>Decrement</button> </div> ); }); export default Counter; // In your App: // <StoreProvider> // <Counter /> // </StoreProvider> """ ## 3. Data Persistence and Hydration ### 3.1. "mobx-persist-store" or Similar Libraries **Standard:** Utilize libraries like "mobx-persist-store" for persisting and rehydrating MobX state. * **Do This:** * Use "mobx-persist-store" or a similar library to persist state to localStorage, sessionStorage, or other storage mechanisms. * Properly configure the library to serialize and deserialize observable properties. * Consider the security implications of storing sensitive data in local storage. * **Don't Do This:** * Manually serialize and deserialize MobX state. * Store sensitive data in local storage without proper encryption. * Persist large or complex data trees to local storage without careful consideration of performance and memory usage. **Why:** Data persistence ensures that application state is preserved across sessions, improving user experience and data integrity. **Example (using "mobx-persist-store"):** """typescript import { makeObservable, observable, action } from 'mobx'; import { create, persist } from 'mobx-persist-store'; class SettingsStore { @persist('localStorage') @observable theme = 'light'; constructor() { makeObservable(this); create({ storage: localStorage, jsonify: true }).then(() => { console.log('SettingsStore hydrated'); }); } @action toggleTheme = () => { this.theme = this.theme === 'light' ? 'dark' : 'light'; }; } const settingsStore = new SettingsStore(); export default settingsStore; """ ### 3.2. Server-Side Rendering (SSR) Considerations **Standard:** Handle state hydration carefully in server-side rendered applications. * **Do This:** * Ensure state is properly serialized and transferred from the server to the client. * Use a suitable mechanism (e.g., "window.__PRELOADED_STATE__") to pass initial state. * Hydrate stores on the client-side before rendering React components. * Be extremely careful about sharing state between different user requests on the server. Scoped instances are absolutely essential! * **Don't Do This:** * Fail to hydrate stores correctly, leading to inconsistencies between server and client. * Accidentally share state between different users in an SSR environment. **Why:** Correct state hydration in SSR applications ensures a consistent initial render on both the server and the client, improving SEO and user experience. **Example (Next.js with MobX):** (Conceptual example - adapting to Next.js specifics is critical.) 1. **Server-Side (getStaticProps/getServerSideProps):** """typescript // pages/index.tsx import { GetStaticProps } from 'next'; import { initializeStore } from '../store'; export const getStaticProps: GetStaticProps = async (context) => { const mobxStore = initializeStore(); //Potentially fetch the data to set a property on the store here return { props: { initialMobxState: mobxStore, // or just .property if that's all that matters }, revalidate: 10, } } """ 2. **Client-Side (custom App):** """typescript // _app.tsx or _app.js import React from 'react'; import { useStore } from '../store'; import { observer } from 'mobx-react-lite'; function MyApp({ Component, pageProps }) { const store = useStore(pageProps.initialMobxState); return ( <Component {...pageProps} /> ) } export default MyApp """ ## 4. Testing ### 4.1. Unit Testing with Mocked Stores **Standard:** Write unit tests for MobX stores using mocked dependencies. * **Do This:** * Use a testing framework like Jest or Mocha. * Mock external dependencies (e.g., API services) to isolate store logic. * Assert the behavior of observables and actions. Ensure state transitions happen as expected. * **Don't Do This:** * Test stores with real dependencies, making tests slow and brittle. * Ignore edge cases and error conditions. **Why:** Unit tests ensure that individual stores function correctly in isolation, improving code reliability and maintainability. **Example (Jest with mocked API):** """typescript import { TodoList } from './todoList'; import { makeObservable, observable, action } from 'mobx'; // Mock the API service const mockApiService = { fetchTodos: jest.fn().mockResolvedValue([{ id: 1, title: 'Test Todo' }]), }; describe('TodoList', () => { let todoList: TodoList; beforeEach(() => { todoList = new TodoList(mockApiService); }); it('should load todos from the API', async () => { await todoList.loadTodos(); expect(mockApiService.fetchTodos).toHaveBeenCalled(); expect(todoList.todos.length).toBe(1); expect(todoList.todos[0].title).toBe('Test Todo'); }); it('should set loading state while loading todos', () => { const promise = todoList.loadTodos(); expect(todoList.loading).toBe(true); return promise.then(() => { expect(todoList.loading).toBe(false); }); }); }); """ ### 4.2. Component Testing with "mobx-react" **Standard:** Test React components that use MobX observables. * **Do This:** * Use a component testing library like React Testing Library or Enzyme. * Provide mocked stores to the components under test using React Context or similar dependency injection mechanisms. * Assert that components update correctly when observables change. * **Don't Do This:** * Test components without providing mocked stores. This can lead to integration tests hiding the state of things. * Rely on implementation details instead of testing component behavior. **Why:** Component tests ensure that React components integrate correctly with MobX observables, improving UI reliability and preventing rendering issues. **Example (React Testing Library with mocked store):** """typescript import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { useLocalObservable, observer } from 'mobx-react-lite'; //Mock Provider import { StoreContext } from './StoreContext'; //adjust path const renderWithStore = (component, store) => { return render( <StoreContext.Provider value={store}> {component} </StoreContext.Provider> ); }; // The Counter component (similar to previous examples) const Counter = observer(() => { //Implementation of the Counter Component }); describe('Counter Component', () => { it('should increment the count when the button is clicked', () => { const mockedStore = useLocalObservable(() => ({ count: 0, increment() { this.count++; }, })); renderWithStore(<Counter />, mockedStore); const incrementButton = screen.getByText('Increment'); fireEvent.click(incrementButton); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); }); """ ## 5. Architectural Patterns and Scalability ### 5.1. Modular Store Design **Standard:** Structure MobX stores into smaller, modular units. * **Do This:** * Divide large stores into smaller, focused stores based on business logic or UI components. * Use composition or aggregation to combine stores as needed. * Avoid creating monolithic stores that manage too much state. * **Don't Do This:** * Create single, massive stores that are difficult to maintain. * Mix unrelated state and logic within the same store. **Why:** Modular store design improves code organization, maintainability, and testability, making it easier to scale applications. The SoC principle applies here too! **Example (Modular Stores):** """typescript // userStore.ts import { makeObservable, observable, action } from 'mobx'; class UserStore { @observable user = null; constructor() { makeObservable(this); } @action setUser = (user) => { this.user = user; }; } export default UserStore; // productStore.ts import { makeObservable, observable, action } from 'mobx'; class ProductStore { @observable products = []; constructor() { makeObservable(this); } @action setProducts = (products) => { this.products = products; }; } export default ProductStore; // combined store (appStore.ts) import UserStore from './userStore'; import ProductStore from './productStore'; class AppStore { userStore: UserStore; productStore: ProductStore; constructor() { this.userStore = new UserStore(); this.productStore = new ProductStore(); } } export default AppStore; """ ### 5.2. Scalable State Management **Standard:** Choose appropriate state management patterns based on application complexity. * **Do This:** * Use simple observables and computed values for small applications. * Utilize more advanced patterns like "flow", actions, and "makeAutoObservable" for complex applications with asynchronous operations and intricate data dependencies. * Consider using a state management library like Rematch or Zustand (although not strictly MobX) if the complexity warrants it. Evaluate whether a full-fledged solution is more appropriate than just MobX by itself. * **Don't Do This:** * Overcomplicate state management in small applications. * Use simple observables for complex applications that require more robust state management. **Why:** Choosing the right state management pattern ensures that applications can scale efficiently without sacrificing performance or maintainability. By adhering to these coding standards, developers can create robust, maintainable, and performant MobX applications. The focus on tools specific to MobX allows teams to quickly find high-quality resources for enhancing their development workflows.
# Deployment and DevOps Standards for MobX This document outlines the deployment and DevOps standards for MobX applications, ensuring maintainability, performance, and reliability throughout the application lifecycle. It focuses on best practices for build processes, CI/CD pipelines, and production considerations specifically related to MobX's reactive nature and state management. ## 1. Build Processes and Optimization ### 1.1. Code Transpilation and Bundling **Standard:** Use modern bundlers like Webpack, Parcel, or Rollup with appropriate configurations for tree-shaking and code splitting. Transpile your code using Babel or TypeScript to ensure compatibility with target environments. **Why:** Modern bundlers optimize your codebase by removing dead code (tree-shaking), splitting your application into smaller chunks for faster initial load times (code-splitting), and transpiling modern JavaScript syntax to be compatible with older browsers. **Do This:** """javascript // webpack.config.js const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/plugin-proposal-class-properties', ['@babel/plugin-proposal-decorators', { legacy: true }]], }, }, }, ], }, optimization: { splitChunks: { chunks: 'all', }, }, }; """ **Don't Do This:** Rely on a single monolithic bundle without code splitting, which can lead to slow initial load times. Avoid using outdated or unconfigured build tools that do not support modern optimization techniques. ### 1.2. Minimization and Compression **Standard:** Minify your bundled JavaScript and CSS files to reduce file sizes. Compress your assets using gzip or Brotli for efficient delivery over the network. **Why:** Minimization reduces file sizes by removing unnecessary whitespace and shortening variable names, while compression further reduces the size of the files transferred over the network. **Do This:** * Configure your bundler (Webpack, Parcel, etc.) to minify JavaScript and CSS in production mode. * Use a compression middleware (e.g., "compression" for Node.js) to compress assets on the server. """javascript // webpack.config.js (Production Mode) module.exports = { mode: 'production', // Enables optimizations like minification // ... other configurations }; // server.js (using compression middleware) const express = require('express'); const compression = require('compression'); const app = express(); app.use(compression()); // Enable gzip compression app.use(express.static('public')); // Serve static assets app.listen(3000, () => console.log('Server running on port 3000')); """ **Don't Do This:** Skip minification and compression, especially for production builds. Serve uncompressed assets, which significantly increases load times. ### 1.3. Environment Variables **Standard:** Use environment variables to configure your application for different environments (development, staging, production). **Why:** Environment variables allow you to externalize configuration settings, making it easier to deploy your application to different environments without modifying the code. **Do This:** * Use "dotenv" package in your development environment for managing environment variables. Store sensitive information in secure configuration management systems like HashiCorp Vault for production. * Access environment variables using "process.env". """javascript // .env (development) API_URL=http://localhost:3001/api NODE_ENV=development // webpack.config.js (using environment variables) const webpack = require('webpack'); require('dotenv').config(); module.exports = { // ... other configurations plugins: [ new webpack.DefinePlugin({ 'process.env.API_URL': JSON.stringify(process.env.API_URL), }), ], }; // app.js (accessing environment variables) const apiUrl = process.env.API_URL; console.log("API URL: ${apiUrl}"); """ **Don't Do This:** Hardcode configuration values directly into your code. Commit sensitive information (API keys, passwords) to your version control repository. ### 1.4. Source Maps **Standard:** Generate source maps for debugging production builds. Store source maps securely and restrict access to authorized personnel. **Why:** Source maps allow you to debug minified and bundled code by mapping the production code back to your original source files. **Do This:** * Configure your bundler to generate source maps (e.g., "devtool: 'source-map'" in Webpack). * Store source maps separately and securely, and configure your error tracking tools (e.g., Sentry) to use them for debugging. * Consider using "hidden-source-map" in production, which generates sourcemaps but doesn't link them in the bundle, requiring them to be uploaded to error tracking services separately. """javascript // webpack.config.js module.exports = { devtool: 'source-map', // Generate source maps // ... other configurations }; """ **Don't Do This:** Expose source maps publicly, which can reveal your source code to unauthorized users. Skip generating source maps altogether, making it difficult to debug production issues. ### 1.5. Asset Versioning (Cache Busting) **Standard:** Implement asset versioning by adding a hash to your bundled filenames. **Why:** Versioning forces browsers to download new versions of your assets when they change, preventing users from seeing outdated content due to browser caching. **Do This:** * Configure your bundler to add a hash to your output filenames. * Use a tool or script to update your HTML files with the new asset filenames. """javascript // webpack.config.js module.exports = { output: { filename: 'bundle.[contenthash].js', // ... other configurations }, }; """ **Don't Do This:** Rely on the browser to automatically update cached assets. Fail to update your HTML files with the new asset filenames, leading to broken links. ## 2. CI/CD Pipelines ### 2.1. Automated Testing **Standard:** Integrate automated tests into your CI/CD pipeline to ensure code quality and prevent regressions. **Why:** Automated tests (unit, integration, end-to-end) provide rapid feedback on code changes, reducing the risk of introducing bugs into your production environment. **Do This:** * Set up unit tests for individual components and MobX stores. * Implement integration tests to verify the interactions between different parts of your application. * Use end-to-end tests to simulate user interactions and ensure the application behaves as expected. * Use tools like Jest, Mocha, Cypress, or Selenium for automated testing. """javascript // Example Jest unit test for a MobX store import CounterStore from '../src/CounterStore'; describe('CounterStore', () => { it('should increment the counter', () => { const counterStore = new CounterStore(); counterStore.increment(); expect(counterStore.count).toBe(1); }); it('should decrement the counter', () => { const counterStore = new CounterStore(); counterStore.decrement(); expect(counterStore.count).toBe(-1); }); }); """ **Don't Do This:** Skip automated testing, leading to frequent regressions and unreliable releases. Rely solely on manual testing, which is time-consuming and error-prone. ### 2.2. Code Linting and Formatting **Standard:** Enforce code linting and formatting rules using tools like ESLint and Prettier to maintain code consistency and prevent common errors. **Why:** Linting and formatting tools automatically check your code for stylistic errors and potential bugs, ensuring code quality and consistency across the team. **Do This:** * Configure ESLint with recommended rules for React and MobX. * Use Prettier to automatically format your code to a consistent style. * Integrate linting and formatting into your CI/CD pipeline to catch issues early. """javascript // .eslintrc.js module.exports = { parser: '@babel/eslint-parser', // Use Babel to parse the code extends: [ 'eslint:recommended', 'plugin:react/recommended', ], plugins: ['react'], rules: { 'react/prop-types': 'off', // Disable prop-types validation }, settings: { react: { version: 'detect', // Automatically detect the React version }, }, env: { browser: true, // Enable browser environment node: true, // Enable Node.js environment es6: true, // Enable ES6 features }, }; // .prettierrc.js module.exports = { semi: true, // Add semicolons at the end of statements trailingComma: 'all', // Add trailing commas in multiline arrays and objects singleQuote: true, // Use single quotes instead of double quotes printWidth: 120, // Maximum line length tabWidth: 2 // Number of spaces per indentation level }; """ **Don't Do This:** Ignore linting and formatting, leading to inconsistent code styles and potential errors. Allow code with linting or formatting errors to be merged into the main branch. ### 2.3. Continuous Integration **Standard:** Implement a CI/CD pipeline using tools like Jenkins, CircleCI, GitHub Actions, or GitLab CI to automate the build, test, and deployment processes. **Why:** CI/CD pipelines automate the entire software release process, enabling faster and more reliable deployments. **Do This:** * Configure your CI/CD pipeline to run automated tests, linting, and formatting checks on every code commit. * Automate the build process, including transpilation, bundling, minification, and compression. * Automate the deployment process to staging and production environments. """yaml # .github/workflows/main.yml (GitHub Actions example) name: CI/CD Pipeline on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Use Node.js 16 uses: actions/setup-node@v3 with: node-version: 16 - name: Install dependencies run: npm install - name: Run linters run: npm run lint - name: Run tests run: npm run test - name: Build run: npm run build deploy: needs: build runs-on: ubuntu-latest steps: - name: Deploy to Production run: echo "Deploying to production..." # Add deployment steps here """ **Don't Do This:** Deploy manually, which is error-prone and time-consuming. Skip automated testing and other checks in your CI/CD pipeline, leading to unreliable releases. ### 2.4. Immutable Deployments **Standard:** Ensure deployment artifacts are immutable and uniquely versioned. Use containerization technologies like Docker to package your application and its dependencies. **Why:** Immutable deployments guarantee that the same artifact is deployed across all environments, eliminating potential discrepancies due to environment differences. **Do This:** * Use Docker to containerize your application and its dependencies. * Tag your Docker images with a unique version number or commit hash. * Deploy the same Docker image to all environments. """dockerfile # Dockerfile FROM node:16-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . ENV NODE_ENV production RUN npm run build EXPOSE 3000 CMD ["node", "dist/server.js"] """ **Don't Do This:** Deploy different versions of your application to different environments. Modify deployment artifacts after they have been built. ### 2.5. Rollback Strategy **Standard:** Implement a clear rollback strategy in case of deployment failures or critical issues. Use blue/green deployments or feature flags to minimize the impact of failed deployments. **Why:** A rollback strategy allows you to quickly revert to a previous working version of your application, minimizing downtime and impact on users. **Do This:** * Implement blue/green deployments, where you maintain two identical environments (blue and green) and switch traffic between them. * Use feature flags to gradually roll out new features and quickly disable them if issues arise. * Maintain a backup of your application and database. ## 3. Production Considerations ### 3.1. Performance Monitoring **Standard:** Implement comprehensive performance monitoring to identify and address performance bottlenecks in your MobX application. **Why:** Performance monitoring allows you to proactively identify and resolve performance issues, ensuring a smooth user experience. **Do This:** * Use tools like Google Analytics, New Relic, or Sentry to track user behavior and application performance. * Monitor key metrics such as response times, error rates, and resource utilization. * Implement client-side performance monitoring to track the performance of your MobX components. Use the MobX "spy" function for debugging and performance analysis, but remove or disable it in production to avoid overhead. """javascript // Example using MobX spy (for development/staging only) import { spy } from "mobx" spy(event => { if (event.type === 'action') { console.log(event) } }) """ **Don't Do This:** Rely solely on anecdotal feedback to identify performance issues. Ignore performance alerts, which can lead to significant performance degradations. ### 3.2. Error Tracking **Standard:** Integrate error tracking tools like Sentry or Bugsnag to capture and analyze errors in your production environment. **Why:** Error tracking tools provide detailed information about errors, including stack traces and user context, allowing you to quickly diagnose and fix issues. **Do This:** * Configure your error tracking tool to capture all uncaught exceptions and unhandled promise rejections. * Use source maps to de-minify stack traces and identify the exact location of errors in your source code. * Monitor error rates and prioritize fixing the most frequent and impactful errors. ### 3.3. Logging **Standard:** Implement robust logging to capture important events and debug issues in your production environment. **Why:** Logging provides valuable insights into the behavior of your application, allowing you to trace errors, monitor performance, and audit user activity. **Do This:** * Use a logging library like Winston or Morgan to log important events, such as user logins, API requests, and error messages. * Log structured data (e.g., JSON) to make it easier to analyze logs. * Rotate your log files to prevent them from consuming too much disk space. * Centralize your logs using a tool like Elasticsearch or Splunk to make it easier to search and analyze them. ### 3.4. Security Hardening **Standard:** Implement security best practices to protect your MobX application from common security threats. **Why:** Security hardening protects your application and its data from unauthorized access, data breaches, and other security incidents. **Do This:** * Follow the OWASP Top 10 guidelines to prevent common web application vulnerabilities. * Use HTTPS to encrypt all communication between the client and the server. * Validate and sanitize all user input to prevent cross-site scripting (XSS) and SQL injection attacks. * Implement authentication and authorization to control access to sensitive resources. * Regularly update your dependencies to patch security vulnerabilities. * Store ALL secrets and keys in a secure vault. * Be aware of potential XSS vulnerabilities when rendering user-provided data within MobX-managed components. Ensure proper escaping and sanitization. ### 3.5. Database Management **Standard:** Employ best practices for database management, including backups, indexing, and query optimization. It is important to manage the data that MobX is observing, so apply solid database practices here. **Why:** Proper database management ensures data integrity, availability, and performance. **Do This:** * Regularly back up your database to prevent data loss. * Use indexes to optimize query performance. * Monitor database performance and optimize slow queries. * Use connection pooling to reduce the overhead of establishing database connections. * Encrypt sensitive data in the database. ### 3.6. Caching **Standard:** Implement caching strategies to improve performance and reduce load on your servers. **Why:** Caching reduces the number of requests to your servers and databases, improving response times and reducing resource consumption. **Do This:** * Use browser caching to cache static assets. * Use server-side caching (e.g., Redis or Memcached) to cache frequently accessed data. * Use content delivery networks (CDNs) to cache and deliver static assets from geographically distributed locations. * Be mindful of MobX's reactivity when caching data. Ensure that cached data is invalidated and updated when the underlying data changes. If using "autorun" to sync with a cache, throttle or debounce the updates to prevent excessive invalidations. ### 3.7. Feature Flags **Standard:** Implement feature flags that allow enabling or disabling new features without deploying new code. **Why**: Enabling feature flags allows easy testing of new features with only a subset of users, performing A/B testing, and quickly disabling features if problems arise. **Do This**: * Use a feature flag management system such as LaunchDarkly, Split.io, or implement your own * Use feature flags to wrap potentially problematic MobX code. * Regularly clean up any flags that are no longer necessary. """ javascript import { useFeatureFlag } from './featureFlagContext'; function MyComponent(){ const isNewFeatureEnabled = useFeatureFlag("new_feature"); return ( <> {isNewFeatureEnabled ? <div>This is the new feature</div>: <div>This is the old feature</div>} </> ) } """ ## 4. MobX Specific Deployment Considerations ### 4.1 MobX Devtools in Production **Standard:** Never include MobX Devtools in production builds. **Why:** MobX Devtools is designed for debugging and development. Including it in production adds unnecessary overhead and potential security risks. **Do This:** * Ensure that the MobX Devtools are only included in development or staging environments using conditional imports or build configurations. """javascript //Conditional load devtools in development mode if (process.env.NODE_ENV === 'development') { require('mobx-react-devtools'); } """ **Don't Do This:** Include MobX Devtools in production builds, as it can significantly impact performance and expose sensitive information. ### 4.2. Managing Large Observables **Standard:** Be mindful of large observable arrays or objects, and implement appropriate strategies for handling them. **Why:** Large observables can impact performance, especially if they are frequently updated. **Do This:** * Use pagination or virtualization techniques to avoid rendering large lists of data at once. * Use "useMemo" appropriately within the UI to avoid unnecessary re-renders. * Consider using tree-like structures or optimized data structures to store large amounts of data. Consider using "mobx-utils" to address performance concerns by utilizing functions such as "fromPromise". ### 4.3. Memory Leaks **Standard:** Ensure that you properly dispose of disposables and reactions to prevent memory leaks. **Why:** MobX reactions can cause memory leaks if they are not properly disposed of. **Do This:** * Use "autorun", "reaction", or "when" with caution, and always dispose of them when they are no longer needed. Store the return value from the functions and call it to dispose of the reaction * Use "observer" with function components and hooks rather than class based components when applicable * Use the "onBecomeObserved" and "onBecomeUnobserved" hooks instead of componentDidMount useEffect etc when possible """javascript import { autorun } from 'mobx'; import { useEffect } from 'react'; function MyComponent({ store }) { useEffect(() => { const disposer = autorun(() => { console.log('Value:', store.value); }); return () => { disposer(); // Dispose of the reaction on unmount }; }, [store]); return <div>My Component</div>; } """ **Don't Do This:** Forget to dispose of reactions, which can lead to memory leaks and performance issues. ### 4.4. SSR and Hydration **Standard:** When using Server-Side Rendering (SSR), ensure that the MobX state is properly serialized and rehydrated on the client. **Why:** SSR requires the initial state to be rendered on the server and then rehydrated on the client to ensure a seamless user experience. **Do This:** * Use "serialize" and "deserialize" methods to persist and restore the MobX state to the client. * Ensure the server and client versions of the MobX store are consistent. """javascript // Server-side (serializing state) import { serialize } from 'mobx-state-tree'; const store = createStore(); // ... populate the store ... const serializedState = serialize(store); // Client-side (hydrating state) import { deserialize } from 'mobx-state-tree'; const store = createStore(); deserialize(store, window.__INITIAL_STATE__); """ **Don't Do This:** Neglect to serialize and rehydrate the MobX state when using SSR, leading to inconsistencies between the server-rendered and client-rendered content. ### 4.5. Context API **Standard:** Use the React Context API strategically for providing MobX stores to components, but avoid overusing it to prevent unnecessary re-renders. Consider using "Provider" tags lower in the rendering tree, too. **Why:** Context API enables easier sharing of state, but global context can cause unnecessary re-renders if not used judiciously. This is especially important because of MobX component re-rendering on state change. **Do This:** * Provide stores only to components that need them using the context provider only on sections of the app. * Use multiple context providers for different parts of the application when applicable. """javascript import React from 'react'; import { CounterStore } from './CounterStore'; const CounterContext = React.createContext(null); export const CounterProvider = ({ children, store }) => ( <CounterContext.Provider value={store}>{children}</CounterContext.Provider> ); export const useCounterStore = () => React.useContext(CounterContext); export default CounterContext; """ ### 4.6 State Resetting **Standard**: Ensure a mechanism in your CI/CD to reset the state of your variables / stores in the production environment for testing purposes. **Why**: You want to be able to test different states of your application based on different tests in the pipeline. """ javascript // Resetting a store in a test environment. Mocking functionality would work as well. import { reset } from "../myStore"; import { runInAction } from "mobx"; expect(store.items.length).toBe(1); runInAction(() => { store.items = []; }); expect(store.items.length).toBe(0); """ By adhering to these Deployment and DevOps standards, we can ensure that our MobX applications are deployed with confidence, perform optimally, and are easy to maintain throughout their lifecycle.
# Component Design Standards for MobX This document outlines the recommended coding standards for designing React components that seamlessly integrate with MobX for state management. The goal is to promote reusable, maintainable, and performant components within a MobX-driven application using the latest features of MobX. ## 1. General Principles ### 1.1. Separation of Concerns * **Do This:** Separate the component's presentational logic (rendering) from its data and behavior (state management). MobX observes the state and triggers re-renders when necessary, simplifying the component's role. * **Don't Do This:** Avoid directly modifying the MobX store from within the render function. This creates side effects and makes debugging difficult. * **Why:** Improves testability, reduces complexity, and enables easier re-use of components in different contexts. ### 1.2. Single Responsibility Principle * **Do This:** Design components with a singular, well-defined purpose. A component should primarily be responsible for rendering UI related to a specific data domain. * **Don't Do This:** Avoid creating "god components" that manage multiple unrelated pieces of state and render complex UIs. * **Why:** Easier to reason about the component's behavior and facilitates modifications without affecting other parts of the application. ### 1.3. Composition over Inheritance * **Do This:** Favor component composition rather than relying on complex inheritance hierarchies. MobX state can be passed down to child components effectively without inheritance. * **Don't Do This:** Avoid creating large inheritance trees of MobX-aware components. * **Why:** More flexible, easier to understand, and promotes reusability. ### 1.4. Declarative UI * **Do This:** Focus on describing the desired UI state based on the MobX store data rather than imperatively manipulating the DOM directly. * **Don't Do This:** Avoid using "ReactDOM.findDOMNode" or other imperative methods to modify the DOM in response to MobX state changes. * **Why:** Aligns with React's philosophy and allows MobX to manage updates efficiently. ## 2. Connecting Components to MobX State ### 2.1. Using "observer" * **Do This:** Wrap your components with the "observer" higher-order component from "mobx-react-lite" (or "mobx-react" for older React versions) to make them reactive to MobX state changes. * **Don't Do This:** Attempt to manually subscribe and unsubscribe from MobX observables within the component. * **Why:** "observer" automatically optimizes re-renders based on which observables are accessed during the rendering process. """javascript import { observer } from 'mobx-react-lite'; import React from 'react'; const TodoItem = observer(({ todo }) => { return ( <li> <input type="checkbox" checked={todo.completed} onChange={() => todo.toggle()} /> {todo.title} </li> ); }); export default TodoItem; """ ### 2.2. Minimize Rendering Overhead * **Do This:** Ensure that components only re-render when their relevant MobX dependencies change. Use "React.memo" in conjunction with "observer" where appropriate to prevent unnecessary re-renders. Consider "useMemo" for derived values or computationally expensive operations within the component. * **Don't Do This:** Pass large, complex objects as props to components when only a small part of the object is actually used for rendering. This can trigger unnecessary re-renders. * **Why:** Optimizes performance, especially in large applications with frequent state updates. """javascript import { observer } from 'mobx-react-lite'; import React, { useMemo } from 'react'; const TodoItem = observer(({ todo }) => { const formattedTitle = useMemo(() => { console.log("Formatting title..."); // Only logs when todo.title changes return todo.title.toUpperCase(); }, [todo.title]); return ( <li> <input type="checkbox" checked={todo.completed} onChange={() => todo.toggle()} /> {formattedTitle} </li> ); }); export default React.memo(TodoItem); // Memoize the component """ ### 2.3. Deriving Data with "useLocalObservable" * **Do This:** Use "useLocalObservable" to manage component-specific state that doesn't need to be globally accessible. This is useful for UI-related state that is not relevant to other parts of the application. * **Don't Do This:** Over-rely on global MobX stores for managing purely local component state. * **Why:** Prevents unnecessary updates to global stores and reduces the risk of unintended side effects. Keeps the global store clean. """javascript import { observer } from 'mobx-react-lite'; import React from 'react'; import { useLocalObservable } from 'mobx-react-lite'; const Counter = observer(() => { const localState = useLocalObservable(() => ({ count: 0, increment() { this.count++; }, decrement() { this.count--; }, })); return ( <div> <button onClick={() => localState.decrement()}>-</button> <span>{localState.count}</span> <button onClick={() => localState.increment()}>+</button> </div> ); }); export default Counter; """ ### 2.4. Use Event Handlers Carefully * **Do This:** Use arrow functions or "useCallback" to avoid creating new functions on every render when passing event handlers to child components. * **Don't Do This:** Define event handlers directly within the JSX, as this creates unnecessary function instances on each render, potentially breaking memoization and causing performance issues. * **Why:** Ensures that child components only re-render when their dependencies truly change. """javascript import React, { useCallback } from 'react'; import { observer } from 'mobx-react-lite'; const MyComponent = observer(({ store }) => { const handleClick = useCallback(() => { store.updateData(); }, [store]); return ( <button onClick={handleClick}>Update Data</button> ); }); export default MyComponent; """ ## 3. Component Structure and Organization ### 3.1. Container/Presentational Pattern * **Do This:** Structure your components using the container/presentational pattern (also known as smart/dumb components). Container components are responsible for fetching data and managing state with MobX, while presentational components focus solely on rendering UI based on props. * **Don't Do This:** Mix data fetching, state management, and UI rendering in a single component. * **Why:** Promotes separation of concerns, improves testability, and enhances reusability. """javascript // Container component import React from 'react'; import { observer } from 'mobx-react-lite'; import { useStore } from './storeContext'; // Assume a context provides access to the MobX store import UserList from './UserList'; // Presentational component const UserListContainer = observer(() => { const { userStore } = useStore(); if (userStore.isLoading) { return <p>Loading...</p>; } return <UserList users={userStore.users} />; }); export default UserListContainer; // Presentational component import React from 'react'; const UserList = ({ users }) => { return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }; export default UserList; """ ### 3.2. Atomic Design Principles * **Do This:** Consider atomic design principles when structuring your components. Break down the UI into small, reusable components (atoms, molecules, organisms, templates, and pages). * **Don't Do This:** Create monolithic components that are difficult to understand and reuse. * **Why:** Improves maintainability and allows for building complex UIs from smaller, independent pieces. ### 3.3. Component Naming Conventions * **Do This:** Use descriptive and consistent naming conventions for components. Component filenames should match their default export name. Differentiate container components (e.g., "UserListContainer") from presentational components (e.g., "UserList"). * **Don't Do This:** Use vague or ambiguous names that don't clearly indicate the component's purpose. * **Why:** Improves code readability and makes it easier to locate and understand components. ## 4. Interacting with MobX Stores ### 4.1. Dependency Injection * **Do This:** Use dependency injection (e.g., React Context) to provide components with access to MobX stores. This makes it easier to test components in isolation and reduces coupling. * **Don't Do This:** Directly import MobX stores into components. This creates tight coupling and makes testing difficult. * **Why:** Promotes loose coupling, improves testability, and allows for easier swapping of stores. """javascript // storeContext.js import React, { createContext, useContext } from 'react'; import { UserStore } from './UserStore'; // Your MobX store const StoreContext = createContext(null); export const StoreProvider = ({ children }) => { const userStore = new UserStore(); // Instantiate your store here return ( <StoreContext.Provider value={{ userStore }}> {children} </StoreContext.Provider> ); }; export const useStore = () => { const store = useContext(StoreContext); if (!store) { // If your component isn’t a child of StoreProvider throw new Error('useStore must be used within a StoreProvider.'); } return store; }; // App.js or index.js import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import { StoreProvider } from './storeContext'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <StoreProvider> <App /> </StoreProvider> ); // MyComponent.js (consuming the store) import React from 'react'; import { observer } from 'mobx-react-lite'; import { useStore } from './storeContext'; const MyComponent = observer(() => { const { userStore } = useStore(); // Access userStore properties and actions return ( <div> {userStore.users.map(user => ( <div key={user.id}>{user.name}</div> ))} </div> ); }); export default MyComponent; """ ### 4.2. Asynchronous Actions * **Do This:** Use "runInAction" to update MobX observables from within asynchronous actions. This ensures that state updates are batched and applied atomically. Utilize "flow" when the async action needs to observe other observables. * **Don't Do This:** Directly modify MobX observables within asynchronous callbacks without using "runInAction". This can lead to inconsistent state and unexpected re-renders. * **Why:** Prevents race conditions, ensures data consistency, and optimizes performance. """javascript import { runInAction } from 'mobx'; import { flow } from 'mobx'; class UserStore { users = []; isLoading = false; fetchUsers = () => { this.isLoading = true; fetch('/api/users') .then(response => response.json()) .then(data => { runInAction(() => { this.users = data; this.isLoading = false; }); }); } // Alternative: Using flow * fetchUsersFlow(userId) { this.isLoading = true; try { const response = yield fetch("/api/users/${userId}"); const data = yield response.json(); runInAction(() => { this.users = data; this.isLoading = false; }); } catch (error) { runInAction(() => { this.error = error; this.isLoading = false; }); } } fetchUsersFlowGenerator = flow(this.fetchUsersFlow); // Necessary to bind the generator function to the class } """ ### 4.3. Computed Values for Derived Data * **Do This:** Use computed values to derive data from MobX observables. This ensures that derived data is automatically updated whenever its dependencies change. * **Don't Do This:** Redundant calculations in the UI whenever the derived value is needed. * **Why:** Optimizes performance and prevents unnecessary recalculations. """javascript import { makeAutoObservable, computed } from 'mobx'; class CartStore { items = []; constructor() { makeAutoObservable(this); } addItem(item) { this.items.push(item); } get totalPrice() { console.log("Calculating total price..."); // Only calculates when cart changes return this.items.reduce((sum, item) => sum + item.price, 0); } } """ ## 5. Testing ### 5.1. Unit Testing Components * **Do This:** Write unit tests for your components using a testing framework like Jest and a rendering library like React Testing Library. Mock the MobX stores and actions to isolate the component being tested. * **Don't Do This:** Rely solely on end-to-end tests for verifying component behavior. * **Why:** Allows for faster and more targeted testing of individual components. """javascript // Example test using React Testing Library and Jest import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import MyComponent from './MyComponent'; import { useStore } from './storeContext'; // Mock the store context jest.mock('./storeContext', () => ({ useStore: () => ({ myStore: { data: 'initial data', updateData: jest.fn(), }, }), })); describe('MyComponent', () => { it('renders data from the store', () => { render(<MyComponent />); expect(screen.getByText('initial data')).toBeInTheDocument(); }); it('calls updateData when the button is clicked', () => { render(<MyComponent />); fireEvent.click(screen.getByRole('button')); expect(useStore().myStore.updateData).toHaveBeenCalled(); }); }); """ ### 5.2. Testing MobX Stores * **Do This:** Write unit tests for your MobX stores to ensure that state updates and actions behave as expected. * **Don't Do This:** Skip testing the MobX store logic and assume that it works correctly. * **Why:** Ensures the core logic of your application is robust and reliable. ## 6. Performance Optimization ### 6.1. Immutability * **Do This:** While MobX handles reactivity automatically, try to treat your state as immutable where practical for clarity and to help with debugging. Especially when reducers are involved. * **Don't Do This:** Directly mutate complex nested objects within the store without considering the reactivity implications. (MobX 6+ handles deep mutations better, but it's still good practice.) * **Why:** Prevents unexpected side effects and makes it easier to track state changes. ### 6.2. Use Correct Data Structures * **Do this:** Consider using appropriate data structures within your MobX stores. For example, use "observable.map" if you need to frequently look up values by key, as it offers better performance compared to iterating through an "observable.array". * **Don't do this:** Blindly use the same data structure everywhere. Profile and understand the performance characteristics of operations on different data structures to choose the right one for your use case. * **Why:** Choosing the right data structure significantly improves performance especially when your data grows. ### 6.3. Debouncing and Throttling * **Do This:** If a component reacts to rapidly changing MobX values (e.g., input fields), consider debouncing or throttling updates to avoid excessive re-renders. Libraries like "lodash" provide utilities for debouncing and throttling functions. * **Don't Do This:** Immediately update the MobX store on every change event if the updates are frequent and not essential. * **Why:** Reduces the number of re-renders and improves responsiveness. ## 7. Security Considerations ### 7.1. Input Validation * **Do This:** Validate all user inputs before updating the MobX store. This prevents malicious data from being stored and displayed in the UI. * **Don't Do This:** Directly store unfiltered user inputs in the MobX store. * **Why:** Prevents security vulnerabilities such as Cross-Site Scripting (XSS) and SQL injection (if the data is persisted to a database). ### 7.2. Authorization * **Do This:** Implement proper authorization checks before allowing users to modify the MobX store data. Ensure that only authorized users can perform certain actions. * **Don't Do This:** Expose sensitive store data or actions without proper authorization. * **Why:** Prevents unauthorized access and modification of data. ### 7.3. Sensitive Data * **Do This:** Avoid storing sensitive data (e.g., passwords, API keys) directly in the MobX store. Instead, store references or encrypted versions of sensitive data. * **Don't Do This:** Store sensitive information unencrypted in the MobX store. * **Why:** Protects sensitive information from unauthorized access. These coding standards provide a comprehensive guide for building maintainable, performant, and secure React components with MobX. Adhering to these guidelines will promote consistency across your codebase and facilitate seamless collaboration among developers.