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