# 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 Loading...
}
if(userStore.error){
return Error: {userStore.error}
}
return (
{userStore.users.map((user) => (
{user.name}
))}
);
});
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 (
setUsername(e.target.value)} />
setPassword(e.target.value)} />
Login
{authService.loginError && {authService.loginError}}
);
});
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: 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) => {
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 (
{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.
danielsogl
Created Mar 6, 2025
# Security Best Practices Standards for MobX
This document outlines security best practices when developing applications using MobX. It aims to guide developers in writing secure, robust, and maintainable code.
## 1. Introduction
Security is a critical aspect of software development, and MobX applications are no exception. While MobX primarily manages state, how that state is handled, accessed, and manipulated has significant security implications. This guide covers common security vulnerabilities and provides actionable guidelines for protecting MobX applications, with specific examples and anti-patterns to avoid.
## 2. Input Validation and Sanitization
### 2.1 Standard: Validate and sanitize all user inputs before using them to update observable state.
* **Do This:** Implement input validation within your MobX actions before modifying any observable properties. Use established validation libraries or custom validation functions based on the expected data type and range. Sanitize special characters that could lead to injection vulnerabilities.
* **Don't Do This:** Directly update observable properties with user-provided input without any form of validation or sanitization.
* **Why:** Input validation prevents malicious or malformed data from corrupting your application's state and potentially triggering vulnerabilities, such as cross-site scripting (XSS) or SQL injection (if the data is eventually persisted to a database).
**Code Example (Input Validation):**
"""typescript
import { makeObservable, observable, action } from "mobx";
import validator from 'validator'; // Example validation library
class UserProfileStore {
@observable username: string = "";
@observable email: string = "";
constructor() {
makeObservable(this);
}
@action setUsername(username: string) {
if (validator.isAlphanumeric(username) && username.length <= 50) {
this.username = username;
} else {
console.error("Invalid username format. Alphanumeric characters only, max 50 characters.");
// Handle the error appropriately, such as displaying an error message to the user.
}
}
@action setEmail(email: string) {
if (validator.isEmail(email)) {
this.email = email;
} else {
console.error("Invalid email format.");
// Handle the error appropriately.
}
}
}
const userProfileStore = new UserProfileStore();
// Usage
userProfileStore.setUsername("validUsername123"); // Valid update
userProfileStore.setUsername("invalid!@#$%username"); // Triggers error handling
userProfileStore.setEmail("test@example.com"); //Valid update
userProfileStore.setEmail("invalid-email"); //Triggers error handling
"""
**Common Anti-Pattern:**
"""typescript
import { makeObservable, observable, action } from "mobx";
class UserProfileStore {
@observable username: string = "";
constructor() {
makeObservable(this);
}
@action setUsername(username: string) {
// BAD: No validation! Directly updating observable state.
this.username = username;
}
}
"""
### 2.2 Standard: Properly escape data when rendering it in the UI.
* **Do This:** Use templating engines or UI frameworks that automatically escape data to prevent XSS vulnerabilities. React, Vue, and Angular typically handle escaping by default.
* **Don't Do This:** Directly inject data retrieved from MobX stores into the DOM without proper escaping, especially if that data originated from user input.
* **Why:** Escaping ensures that data is treated as plain text and not interpreted as executable code. This significantly reduces the risk of XSS attacks where malicious scripts are injected into your application.
**Code Example (React with JSX, demonstrating automatic escaping):**
"""typescript jsx
import { observer } from "mobx-react-lite";
import { useSnapshot } from "mobx-state-tree"; //Example, not required
interface Props {
username: string;
comment: string;
}
const UserProfile = observer(({ username, comment }: Props) => {
return (
<div>
<p>Username: {username}</p> {/* React automatically escapes this value */}
<p>Comment: {comment}</p> {/* React automatically escapes this value */}
</div>
);
});
export default UserProfile;
"""
**Common Anti-Pattern:**
"""typescript jsx
//Potentially unsafe if the username or comment contains unescaped HTML
const UnsafeUserProfile = observer(({ username, comment }: Props) => {
return (
<div>
<p>Username: {username}</p>
<p>Comment: <dangerouslySetInnerHTML={{ __html: comment }} /></p> {/* VERY DANGEROUS */}
</div>
);
});
"""
## 3. Authorization and Access Control
### 3.1 Standard: Implement proper authorization checks before allowing users to modify sensitive observable properties.
* **Do This:** Use authentication and authorization mechanisms to restrict access to modifying state based on user roles and permissions. Employ middleware or guard functions to verify a user’s authority before executing actions that affect sensitive data.
* **Don't Do This:** Allow any user to modify any part of the application’s state without authentication or authorization.
* **Why:** Authorization limits the ability of unauthorized users to tamper with sensitive data, preventing data corruption or system compromise.
**Code Example (Middleware Authorization):**
"""typescript
import { makeObservable, observable, action } from "mobx";
// Example User Authentication
const getCurrentUserRole = (): string => {
// In a real application, this would fetch the user's role from an authentication service.
return "admin"; // Or "user", or any other role
};
class AdminPanelStore {
@observable sensitiveData: string = "Initial sensitive data";
constructor() {
makeObservable(this);
}
@action.bound updateSensitiveData(newData: string) {
const userRole = getCurrentUserRole();
if (userRole === "admin") {
this.sensitiveData = newData;
} else {
console.warn("Unauthorized access: insufficient privileges.");
// Handle unauthorized access (e.g., throw an error, display a message).
}
}
}
const adminPanelStore = new AdminPanelStore();
"""
**Common Anti-Pattern:**
"""typescript
import { makeObservable, observable, action } from "mobx";
class AdminPanelStore {
@observable sensitiveData: string = "Initial sensitive data";
constructor() {
makeObservable(this);
}
@action.bound updateSensitiveData(newData: string) {
// BAD: No authorization check. Anyone can update the data.
this.sensitiveData = newData;
}
}
"""
### 3.2 Standard: Avoid exposing internal application state directly to external consumers or components that do not require access.
* **Do This:** Encapsulate sensitive data within your MobX stores and provide controlled access through specific, well-defined actions and computed values. Design APIs that only expose the minimum necessary information.
* **Don't Do This:** Expose the entire state object or observable properties directly to all components, giving them unrestricted access to modify or read sensitive data.
* **Why:** Principle of Least Privilege. Restricting access reduces the attack surface and minimizes the impact of potential vulnerabilities in individual components. It also promotes better modularity and maintainability.
**Code Example (Controlled Access via Computed Values and Actions):**
"""typescript
import { makeObservable, observable, computed, action } from "mobx";
class UserStore {
@observable private _userId: string = "";
@observable private _email: string = "";
@observable private _isAdmin: boolean = false; // Sensitive property
constructor() {
makeObservable(this);
}
@computed get userId() {
return this._userId; // Expose only the user ID
}
@computed get email() {
return this._email; // Expose the email
}
// Do NOT provide a direct getter for isAdmin outside of the store
@action setUserId(userId: string) {
this._userId = userId;
}
@action setEmail(email: string) {
this._email = email;
}
@action setAdminStatus(isAdmin: boolean) { //Only allow admin status to be set internally
this._isAdmin = isAdmin;
}
//Internal method to verify admin status
isAdminCheck(userRole: string): boolean {
if (userRole === "admin") {
return this._isAdmin;
}
return false;
}
}
const userStore = new UserStore();
// Components can only access userId and email directly
console.log(userStore.userId);
console.log(userStore.email);
//Direct access to isAdmin is prevented
// userStore.isAdmin //This will not work since no getter for isAdmin
"""
**Common Anti-Pattern:**
"""typescript
import { makeObservable, observable} from "mobx";
class UserStore {
@observable userId: string = ""; // Public property - Easy access
@observable email: string = ""; // Public property- Easy access
@observable isAdmin: boolean = false; // Public and sensitive property - DANGEROUS
constructor() {
makeObservable(this);
}
}
const userStore = new UserStore();
// Component can directly access and potentially misuse isAdmin:
console.log(userStore.isAdmin); // BAD DIRECT ACCESS
"""
## 4. Secure Data Storage
### 4.1 Standard: When persisting data, avoid storing sensitive information directly in local storage or cookies.
* **Do This:** If you must store sensitive data, encrypt it before storing it and use secure storage mechanisms, such as the browser's "localStorage" in combination with encryption libraries, or server-side storage like "HttpOnly" cookies. Use a strong encryption algorithm and manage encryption keys securely (ideally on the server-side).
* **Don't Do This:** Store sensitive data like passwords, API keys, or personal information directly in "localStorage" or cookies without encryption.
* **Why:** "localStorage" and cookies are easily accessible client-side, making them vulnerable to attacks like XSS. Encryption protects data even if these storage locations are compromised. Always opt for secure, server-side storage whenever possible.
**Code Example (Encrypting Data Before Storing in Local Storage):**
"""typescript
import { makeObservable, observable, action } from "mobx";
import CryptoJS from 'crypto-js'; // Example encryption library
const ENCRYPTION_KEY = "YourSecretEncryptionKey"; // Never hardcode keys in production!
class AuthStore {
@observable private _authToken: string | null = null;
constructor() {
makeObservable(this);
this.loadToken(); // Load token from localStorage on initialization
}
@action setAuthToken(token: string) {
this._authToken = token;
this.saveToken(token);
}
@action clearAuthToken() {
this._authToken = null;
localStorage.removeItem("authToken");
}
private saveToken(token: string) {
const encryptedToken = CryptoJS.AES.encrypt(token, ENCRYPTION_KEY).toString();
localStorage.setItem("authToken", encryptedToken);
}
private loadToken() {
const encryptedToken = localStorage.getItem("authToken");
if (encryptedToken) {
try {
const bytes = CryptoJS.AES.decrypt(encryptedToken, ENCRYPTION_KEY);
const decryptedToken = bytes.toString(CryptoJS.enc.Utf8);
if (decryptedToken) {
this._authToken = decryptedToken;
}
} catch (error) {
console.error("Failed to decrypt token:", error);
localStorage.removeItem("authToken"); // Remove corrupted token
}
}
}
get authToken() {
return this._authToken;
}
}
const authStore = new AuthStore();
"""
**Common Anti-Pattern:**
"""typescript
import { makeObservable, observable, action } from "mobx";
class AuthStore {
@observable authToken: string | null = null;
constructor() {
makeObservable(this);
this.loadToken();
}
@action setAuthToken(token: string) {
this.authToken = token;
localStorage.setItem("authToken", token); // BAD: Storing the token directly.
}
@action clearAuthToken() {
this.authToken = null;
localStorage.removeItem("authToken");
}
private loadToken() {
const token = localStorage.getItem("authToken");
if (token) {
this.authToken = token;
}
}
}
const authStore = new AuthStore();
"""
## 5. State Management Security Considerations
### 5.1 Standard: Monitor the state mutations within MobX to detect and respond to potential security breaches.
* **Do This:** Implement auditing and logging mechanisms to track changes to sensitive state properties. Use MobX's "observe" or middleware to monitor actions and react to suspicious activity.
* **Don't Do This:** Assume that the state cannot be manipulated by unauthorized users or malicious code.
* **Why:** Monitoring state mutations allows you to detect and respond to unexpected or unauthorized changes in your application. This can help you identify and mitigate security breaches.
**Code Example (Monitoring State Changes):**
"""typescript
import { makeObservable, observable, action, observe } from "mobx";
class PaymentStore {
@observable creditCardNumber: string = "****-****-****-1234"; // Masked for display
constructor() {
makeObservable(this);
observe(this, "creditCardNumber", (change) => {
//This is a SECURITY RISK! Credit Card numbers should NEVER be logged, printed, or directly observed
console.warn("Credit card number changed:", change); // Audit this change
//Add additional checking logic to verify if this change is from the expected source.
});
}
@action updateCreditCard(newCardNumber: string) {
// Perform validation and security checks here before updating the card.
this.creditCardNumber = newCardNumber;
}
}
const paymentStore = new PaymentStore();
// Usage
paymentStore.updateCreditCard("****-****-****-5678"); // triggers console log
"""
**Common Anti-Pattern:**
"""typescript
import { makeObservable, observable, action} from "mobx";
class PaymentStore {
@observable creditCardNumber: string = "****-****-****-1234"; // Masked for display
constructor() {
makeObservable(this);
}
@action updateCreditCard(newCardNumber: string) {
// No monitoring or auditing.
this.creditCardNumber = newCardNumber;
}
}
"""
### 5.2 Standard: Implement safeguards against CSRF (Cross-Site Request Forgery) attacks when performing state-modifying actions.
* **Do This:** Utilize anti-CSRF tokens or request verification mechanisms to ensure that state-modifying actions are only initiated by authenticated users within your application.
* **Don't Do This:** Trust that requests are legitimate without verifying their origin, especially when dealing with actions that update sensitive application state.
* **Why:** CSRF attacks can trick users into unknowingly performing actions that modify their account or data, leading to unauthorized changes to application state.
**Implementation Note:** CSRF is primarily handled at the server level by validating a token that is sent with each state-modifying request. Ensure that your backend API implements this protection, and integrate it with your MobX actions.
**Code Example (CSRF Protection - Conceptual):**
This example demonstrates the idea and functionality of a CSFR. The actual implementation is highly dependent on your backend.
"""typescript
import { makeObservable, observable, action } from "mobx";
import axios from 'axios'; // Example HTTP client
import Cookies from 'js-cookie'; //To get the csrf token when set as a cookie.
class SettingsStore {
@observable notificationPreferences: string = "email";
constructor() {
makeObservable(this);
}
@action async updateNotificationPreferences(newPreference: string) {
try {
// Get the CSRF token. This is highly dependent on your back end framework.
const csrfToken = Cookies.get('csrftoken');
await axios.post("/api/updatePreferences", {
preference: newPreference,
},
{
headers: {
'X-CSRFToken': csrfToken
}
});
this.notificationPreferences = newPreference;
} catch (error) {
console.error("Failed to update preferences:", error);
// Handle errors appropriately
}
}
}
const settingsStore = new SettingsStore();
"""
**Common Anti-Pattern:**
Omitting server-side CSRF token validation entirely, which would make any state-changing request sent to the backend vulnerable.
## 6. Dependency Management and Security
### 6.1 Standard: Keep your MobX and related dependencies up to date with the latest security patches.
* **Do This:** Regularly audit your project's dependencies and update to the latest versions to incorporate critical security fixes. Use tools like "npm audit" or "yarn audit" to identify and address known vulnerabilities.
* **Don't Do This:** Use outdated versions of MobX or its ecosystem libraries, as they may contain unpatched security vulnerabilities.
* **Why:** Outdated dependencies are a common source of security vulnerabilities. Maintaining up-to-date dependencies minimizes this risk and ensures that you are benefiting from the latest security enhancements.
**Implementation Note:** Use a dependable dependency management strategy and automate the process of checking for and applying updates whenever possible.
### 6.2 Standard: Review the security implications of any third-party libraries or integrations used in conjunction with MobX.
* **Do This:** Before integrating a third-party library, research its security history, assess its reputation, and understand its potential impact on your application's security.
* **Don't Do This:** Blindly integrate any third-party library without considering its security implications.
* **Why:** Third-party libraries can introduce vulnerabilities into your application if they are not well-maintained or have underlying security flaws. Due diligence is crucial to ensure that any dependencies you introduce are secure.
## 7. Handling Errors and Exceptions
### 7.1 Standard: Avoid exposing sensitive information in error messages or logs.
* **Do This:** Implement centralized error handling and logging mechanisms to handle runtime errors and exceptions. When logging errors, redact sensitive information like passwords, API keys, or credit card numbers. Display generic error messages to the user to prevent information leakage.
* **Don't Do This:** Log or display sensitive information directly in error messages, as this could expose it to malicious actors.
* **Why:** Verbose error messages can reveal internal application details that attackers can exploit. Redacting sensitive information prevents this type of leakage.
**Code Example (Redacting Sensitive Information in Error Messages):**
"""typescript
import { makeObservable, observable, action } from "mobx";
import axios from 'axios';
class PaymentStore {
@observable paymentStatus: string = "";
constructor() {
makeObservable(this);
}
@action async processPayment(creditCardNumber: string, amount: number) {
try {
// Simulate an API call that might fail
const response = await axios.post("/api/processPayment", {
creditCardNumber: creditCardNumber,
amount: amount
});
this.paymentStatus = "Payment successful";
} catch (error:any) {
console.error("Payment processing failed. Please notify support.");
console.log(error);
this.paymentStatus = "Payment failed. Please contact support."; // Generic error message
// DON'T log the credit card number or detailed error (may leak info)
}
}
}
const paymentStore = new PaymentStore();
"""
**Common Anti-Pattern:**
"""typescript
import { makeObservable, observable, action } from "mobx";
import axios from 'axios';
class PaymentStore {
@observable paymentStatus: string = "";
constructor() {
makeObservable(this);
}
@action async processPayment(creditCardNumber: string, amount: number) {
try {
// Simulate an API call that might fail
const response = await axios.post("/api/processPayment", {
creditCardNumber: creditCardNumber,
amount: amount
});
this.paymentStatus = "Payment successful";
} catch (error:any) {
console.error("Payment processing failed:", error); // BAD: Might log sensitive information!
this.paymentStatus = "Payment failed. Please contact support.";
}
}
}
"""
## 8. Conclusion
Following these security best practices when developing MobX applications will significantly reduce the risk of vulnerabilities and ensure that your application is secure and robust. Remember that security is an ongoing process, and it's important to stay informed about the latest security threats and best practices. Regular code reviews, security audits, and penetration testing are essential steps in maintaining the security of your MobX applications.
danielsogl
Created Mar 6, 2025
# Code Style and Conventions Standards for MobX
This document outlines code style and conventions standards for MobX-based projects. Adhering to these guidelines will improve code readability, maintainability, and overall project health. These standards are tailored for the latest versions of MobX and aim to leverage modern patterns and best practices. AI coding assistants should be configured to enforce these standards.
## 1. General Formatting and Style
### 1.1 Code Formatting
* **Do This:** Use a code formatter like Prettier to automatically format your code.
* **Don't Do This:** Rely on manual formatting; this is prone to inconsistencies.
**Why?** Consistent formatting improves readability and reduces visual noise, allowing developers to focus on the logic.
**Example:**
"""javascript
// Before formatting
const myObservable = observable(
{ someValue : 123 , anotherValue : 'hello'}
);
// After formatting (with Prettier)
import { observable } from 'mobx';
const myObservable = observable({
someValue: 123,
anotherValue: 'hello',
});
"""
### 1.2 Indentation
* **Do This:** Use 2 spaces for indentation. Configure your editor to use spaces instead of tabs.
* **Don't Do This:** Use tabs or inconsistent indentation.
**Why?** Consistent indentation enhances code structure and readability.
### 1.3 Line Length
* **Do This:** Limit lines to a maximum of 120 characters.
* **Don't Do This:** Write extremely long lines that require horizontal scrolling.
**Why?** Shorter lines are easier to read and fit better on various screen sizes.
### 1.4 Whitespace
* **Do This:** Use whitespace to separate logical code blocks and operators.
* **Don't Do This:** Write dense code without any whitespace.
**Why?** Whitespace improves code legibility and highlights different parts of a statement or function.
**Example:**
"""javascript
// Do This
const result = (a + b) * c;
// Don't Do This
const result=(a+b)*c;
"""
## 2. Naming Conventions
Consistent naming is crucial for understanding code quickly. Focus on clarity and descriptiveness.
### 2.1 Observables
* **Do This:** Name observables using camelCase. Use descriptive names that clarify their purpose. Prefix private observables with an underscore ("_").
* **Don't Do This:** Use cryptic or abbreviated names.
**Why?** Clear naming improves readability and maintainability.
**Example:**
"""javascript
import { observable } from 'mobx';
class Todo {
id = Math.random();
@observable text = "";
@observable completed = false;
@observable _internalState = "pending"; // Private observable
}
"""
### 2.2 Actions
* **Do This:** Use camelCase for actions. Use verbs to describe what the action does. When using asynchronous operations inside actions, append "Async" to the action name.
* **Don't Do This:** Use nouns or ambiguous names for actions.
**Why?** Clear naming of actions makes it easy to determine what affects the state.
**Example:**
"""javascript
import { observable, action } from 'mobx';
class TodoStore {
@observable todos = [];
@action
addTodo(text) {
this.todos.push({ text, completed: false, id: Math.random() });
}
@action
toggleCompletedAsync(todo) {
// simulate an async operation
setTimeout(() => {
todo.completed = !todo.completed;
}, 500);
}
}
"""
### 2.3 Computed Values
* **Do This:** Use camelCase for computed values. Use nouns or adjectives to describe what the computed value represents.
* **Don't Do This:** Name computed values after actions or commands.
**Why?** Consistent naming helps to distinguish computed values from actions.
**Example:**
"""javascript
import { observable, computed } from 'mobx';
class Cart {
@observable items = [];
@computed
get totalQuantity() {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
}
"""
### 2.4 Store Classes
* **Do This:** Use PascalCase (UpperCamelCase) for class names that represent stores. Suffix class names with "Store".
* **Don't Do This:** Use generic names like "Data" or "Manager".
**Why?** Provides a recognizable convention for identifying MobX stores.
**Example:**
"""javascript
class UserStore {
// ...
}
class ProductStore {
// ...
}
"""
### 2.5 Constants
* **Do This:** Use SCREAMING_SNAKE_CASE for constants.
* **Don't Do This:** Use camelCase or PascalCase for constants.
**Why?** Clearly identifies values that should not be modified.
**Example:**
"""javascript
const MAX_ITEMS = 10;
const API_URL = "https://example.com/api";
"""
## 3. MobX-Specific Code Style
### 3.1 Decorators vs. "makeObservable"
* **Do This:** Prefer decorators for defining observables, actions, and computed values when using a modern JavaScript environment that supports them (ES2022+). If decorators are not supported, use "makeObservable".
* **Don't Do This:** Mix decorators and "makeObservable" within the same class unless absolutely necessary.
**Why?** Decorators provide a more concise and readable syntax. "makeObservable" is necessary for environments that do not support decorators or when finer-grained control is required.
**Example (Decorators):**
"""javascript
import { observable, action, computed } from 'mobx';
class CounterStore {
@observable count = 0;
@action
increment() {
this.count++;
}
@computed
get isEven() {
return this.count % 2 === 0;
}
}
"""
**Example ("makeObservable"):**
"""javascript
import { observable, action, computed, makeObservable } from 'mobx';
class CounterStore {
count = 0;
increment() {
this.count++;
}
get isEven() {
return this.count % 2 === 0;
}
constructor() {
makeObservable(this, {
count: observable,
increment: action,
isEven: computed
});
}
}
"""
### 3.2 Explicit Action Boundaries
* **Do This:** Use "@action" or "runInAction" to mark functions or blocks of code that modify observables.
* **Don't Do This:** Directly modify observables outside of actions.
**Why?** Actions provide a clear boundary for state modifications, improving predictability and performance. MobX can batch updates more efficiently when changes are made inside actions.
**Example (Using "@action"):**
"""javascript
import { observable, action } from 'mobx';
class UserStore {
@observable name = "initial name";
@action
updateName(newName) {
this.name = newName;
}
}
"""
**Example (Using "runInAction"):**
"""javascript
import { observable, runInAction } from 'mobx';
class UserStore {
@observable name = "initial name";
fetchUserData = async () => {
const data = await fetchData(); // Assume fetchData is an async function
runInAction(() => {
this.name = data.name;
});
}
}
"""
### 3.3 Use "autorun" Sparingly
* **Do This:** Use "autorun" only for side effects that are difficult to achieve with "@observer" or "reaction".
* **Don't Do This:** Use "autorun" for general-purpose state management.
**Why?** "autorun" is a powerful but potentially inefficient tool. It re-runs whenever any of its dependencies change, which can lead to unnecessary computations. Prefer "@observer" in React components for rendering, and "reaction" for more controlled side effects.
**Example (Appropriate use of "autorun"):**
"""javascript
import { autorun } from 'mobx';
class PrintLogger {
constructor(store) {
this.store = store;
autorun(() => {
console.log("Current count:", store.count);
});
}
}
"""
### 3.4 Use "reaction" for Controlled Side Effects
* **Do This:** Use "reaction" for side effects that need fine-grained control over when they run and what data they react to.
* **Don't Do This:** Rely solely on "autorun" for all side effects.
**Why?** "reaction" allows you to specify both the data to observe and the effect to run, making it more efficient and predictable than "autorun".
**Example:**
"""javascript
import { reaction, observable } from 'mobx';
class DataStore {
@observable data = null;
constructor() {
reaction(
() => this.data, // Data to observe
(data) => { // Side effect to run
if (data) {
console.log("Data updated:", data);
// Perform some other side effect
}
}
);
}
}
"""
### 3.5 Reactivity in React Components
* **Do This:** Wrap React components with "@observer" to automatically re-render when relevant observables change.
* **Don't Do This:** Manually subscribe to observables in React components.
**Why?** "@observer" optimizes rendering by ensuring components only re-render when their dependencies change. This is a core part of MobX's integration with React.
**Example:**
"""javascript
import { observer } from 'mobx-react-lite';
import React from 'react';
const MyComponent = observer(({ store }) => {
return (
<div>
<p>Count: {store.count}</p>
</div>
);
});
export default MyComponent;
"""
### 3.6 Immutable Data Structures for Complex State
Though MobX can handle mutable state effectively, using immutable data structures for complex state can enhance predictability and simplify debugging, specifically when undo/redo functionality, time-travel debugging, or complex state comparisons are needed.
* **Do This:** Consider using libraries like Immer or structural sharing techniques. Immutability becomes particularly useful for complex nested objects or arrays.
* **Don't Do This:** Mutate state directly when using immutable data structures.
**Why?** Immutable data structures prevent accidental state mutations and can simplify complex state management scenarios.
**Example:**
"""javascript
import { observable, action } from 'mobx';
import produce from "immer"
class AppState {
@observable baseState = {nested : {prop : 1}}
constructor() {
makeObservable(this)
}
@action setProp(val) {
this.baseState = produce(this.baseState, draft => {
draft.nested.prop = val
})
}
}
"""
Example without immer would be more complex without Immer:
"""javascript
import { observable, action } from 'mobx';
class AppState {
@observable baseState = {nested : {prop : 1}}
constructor() {
makeObservable(this)
}
@action setProp(val) {
this.baseState = { ...this.baseState, nested: { ...this.baseState.nested, prop: val } };
}
}
"""
### 3.7 Avoid Deeply Nested Observables
* **Do This:** Structure your state so that observables are relatively flat. Break down complex data structures into separate, manageable observables.
* **Don't Do This:** Create extremely deep nested observable structures.
**Why?** Deep nesting can make reactivity less efficient and harder to reason about. Flattening the structure improves performance and maintainability.
**Example (Poor structure):**
"""javascript
import { observable } from 'mobx';
const store = observable({
user: {
profile: {
address: {
street: "Some Street",
city: "Some City"
}
}
}
});
"""
**Example (Improved structure):**
"""javascript
import { observable } from 'mobx';
const store = observable({
userProfile: {
street: "Some Street",
city: "Some City"
}
});
"""
### 3.8 Asynchronous Actions
* **Do This:** Handle asynchronous operations within actions and update observables appropriately. Consider using "try...catch" blocks for error handling.
* **Don't Do This:** Directly modify observables in asynchronous callbacks outside of actions.
**Why?** Ensures that all state modifications are tracked and batched correctly.
**Example:**
"""javascript
import { observable, action, runInAction } from 'mobx';
class DataStore {
@observable isLoading = false;
@observable data = null;
@observable error = null;
@action
fetchData = async () => {
this.isLoading = true;
this.error = null;
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
runInAction(() => {
this.data = data;
this.isLoading = false;
});
} catch (error) {
runInAction(() => {
this.error = error;
this.isLoading = false;
});
}
}
}
"""
### 3.9 Disposing of Reactions and Computed Values
* **Do This:** Dispose of reactions (e.g., "autorun", "reaction") and computed values when they are no longer needed to prevent memory leaks.
* **Don't Do This:** Forget to dispose of resources, especially in components that are frequently mounted and unmounted.
**Why?** Unnecessary reactions and computed values can consume resources and lead to performance issues.
**Example:**
"""javascript
import { autorun } from 'mobx';
import { useEffect } from 'react';
function MyComponent({ store }) {
useEffect(() => {
const disposer = autorun(() => {
console.log("Count:", store.count);
});
return () => {
disposer(); // Dispose of the autorun
};
}, [store]);
return (
<div>
<p>Count: {store.count}</p>
</div>
);
}
"""
## 4. Advanced Patterns
### 4.1 Dependency Injection
* **Do This:** Use dependency injection to provide stores to components. This promotes testability and loose coupling.
* **Don't Do This:** Directly import stores into components.
**Why?** Makes components easier to test in isolation and reduces dependencies. React Context is a great way to implement this pattern.
**Example:**
"""javascript
import React, { createContext, useContext } from 'react';
import { UserStore } from './UserStore';
import { ProductStore } from './ProductStore';
const StoreContext = createContext({
userStore: new UserStore(),
productStore: new ProductStore()
});
export const useStores = () => useContext(StoreContext);
export const StoreProvider = ({ children, userStore, productStore }) => {
const stores = {
userStore: userStore || new UserStore(),
productStore: productStore || new ProductStore(),
};
return (
<StoreContext.Provider value={stores}>
{children}
</StoreContext.Provider>
);
};
// In a component:
import { observer } from 'mobx-react-lite';
import { useStores } from './storeContext';
const MyComponent = observer(() => {
const { userStore, productStore } = useStores();
// ...
});
"""
### 4.2 Optimistic Updates
* **Do This:** Implement optimistic updates when performing actions that modify data on a server. Immediately update the UI and revert if the server request fails.
* **Don't Do This:** Wait for the server response before updating the UI, leading to a laggy user experience.
**Why?** Improves the responsiveness of the application and provides a better user experience.
**Example:**
"""javascript
import { observable, action } from 'mobx';
class TodoStore {
@observable todos = [];
@action
addTodoAsync = async (text) => {
const tempId = Math.random();
const newTodo = { id: tempId, text, completed: false };
this.todos.push(newTodo);
try {
const response = await api.addTodo(text); // Assume api.addTodo returns a promise
runInAction(() => {
newTodo.id = response.id; // Replace tempId with actual ID from server
});
} catch (error) {
runInAction(() => {
this.todos = this.todos.filter(todo => todo.id !== tempId); // Revert on error
});
console.error("Failed to add todo:", error);
}
}
}
"""
By following these coding standards and conventions, MobX projects will exhibit greater consistency, readability, and maintainability, streamlining development efforts and minimizing potential pitfalls. Remember to configure your IDE and AI coding tools (like Github Copilot) to adhere to these rules.
danielsogl
Created Mar 6, 2025
# 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.
danielsogl
Created Mar 6, 2025
# 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.
danielsogl
Created Mar 6, 2025
# 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.
danielsogl
Created Mar 6, 2025