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