React/Mobx应用中如何及何时设置Box领域对象属性?
Hey there! Let's work through this problem the Mobx-friendly way—since you're coming from Redux, we'll focus on maintaining single Box instances, keeping performance snappy, and following Mobx's core principles.
First: Refactor Your Box Class to Be Observable
The key here is to make each Box a standalone observable instance, with its own state that we can update directly (instead of creating new arrays every time). This avoids the performance hit of re-rendering every Box when state changes, and aligns with Mobx's "mutate where it makes sense" philosophy.
import { makeObservable, observable, computed, action } from "mobx"; class Box { id; size; category; x = 0; y = 0; highlighted = false; constructor(data) { this.id = data.id; this.size = data.size; this.category = data.category; makeObservable(this, { x: observable, y: observable, highlighted: observable, displayLabel: computed, // Example computed value for UI setPosition: action, setHighlighted: action }); } // Pure computed value (no side effects) get displayLabel() { return `${this.id} (Size: ${this.size})`; } // Action to safely update position (Mobx requires actions for state changes) setPosition(x, y) { this.x = x; this.y = y; } // Action to toggle highlight state (triggered by mouse events) setHighlighted(isHighlighted) { this.highlighted = isHighlighted; } }
Second: Build a BoxesStore to Manage State & Layout Logic
Your store will handle UI state (sort/filter values), maintain the list of Box instances, and trigger layout updates when needed. We'll use reaction (Mobx's side-effect handler) to run layout calculations only when relevant state changes—this is the right place for side effects like updating Box positions (since computed values should always be pure).
import { makeObservable, observable, action, reaction } from "mobx"; class BoxesStore { boxes = []; sortBy = "size"; // Options: 'size', 'category' filterBy = null; // Filter by category value, null = no filter constructor(initialBoxData) { // Initialize Box instances from raw data this.boxes = initialBoxData.map(data => new Box(data)); makeObservable(this, { boxes: observable, sortBy: observable, filterBy: observable, setSortBy: action, setFilterBy: action, filteredAndSortedBoxes: computed }); // Set up reaction to trigger layout updates when state changes reaction( // Track dependencies: sort/filter values + the list of visible Box IDs () => [this.sortBy, this.filterBy, this.filteredAndSortedBoxes.map(b => b.id)], // Run layout calculation when dependencies change () => this.calculateAndUpdateLayout() ); // Run initial layout this.calculateAndUpdateLayout(); } // Pure computed: returns filtered/sorted boxes (no side effects) get filteredAndSortedBoxes() { let result = [...this.boxes]; // Apply filter if (this.filterBy) { result = result.filter(box => box.category === this.filterBy); } // Apply sort switch (this.sortBy) { case "size": result.sort((a, b) => b.size - a.size); break; case "category": result.sort((a, b) => a.category.localeCompare(b.category)); break; default: break; } return result; } // Actions to update UI state setSortBy(newSortBy) { this.sortBy = newSortBy; } setFilterBy(newFilterBy) { this.filterBy = newFilterBy || null; } // Replace this with your actual force-directed layout algorithm calculateAndUpdateLayout() { const visibleBoxes = this.filteredAndSortedBoxes; // Example: Simple vertical layout (swap with your force-directed logic) visibleBoxes.forEach((box, index) => { box.setPosition(50, 50 + index * 100); }); } }
Third: Integrate with React Components
Use mobx-react-lite's observer HOC to make components reactive—they'll only re-render when the specific observable values they use change.
import { observer } from "mobx-react-lite"; // Individual Box component const BoxComponent = observer(({ box }) => { return ( <div style={{ position: "absolute", left: box.x, top: box.y, width: box.size * 10, height: box.size * 10, backgroundColor: box.highlighted ? "#ffcc00" : "#ccc", border: "1px solid #333", padding: "8px", cursor: "pointer" }} onMouseEnter={() => box.setHighlighted(true)} onMouseLeave={() => box.setHighlighted(false)} > {box.displayLabel} </div> ); }); // Container component with controls const BoxesContainer = observer(({ store }) => { return ( <div style={{ position: "relative", minHeight: "100vh" }}> <div className="controls"> <label> Sort by: <select value={store.sortBy} onChange={(e) => store.setSortBy(e.target.value)} > <option value="size">Size</option> <option value="category">Category</option> </select> </label> <label> Filter by category: <input type="text" value={store.filterBy || ""} onChange={(e) => store.setFilterBy(e.target.value)} /> </label> </div> {store.filteredAndSortedBoxes.map(box => ( <BoxComponent key={box.id} box={box} /> ))} </div> ); });
Why This Works (And Follows Mobx Best Practices)
- Single Box Instances: We never recreate Box objects—we update their observable properties directly, so only the affected components re-render.
- Pure Computeds:
filteredAndSortedBoxesis a pure function with no side effects, which is exactly how computed values should be used. - Side Effects in Reaction: Layout calculations (which modify Box positions) are handled in a
reaction, which only runs when its dependencies change—this avoids unnecessary recalculations and keeps state changes predictable. - Decoupled Logic: Boxes don't need to know about the store; the store manages layout, and Boxes manage their own state (position, highlight).
Quick Answers to Your Original Questions
- Why not set positions in a computed? Computeds should be pure and cacheable—modifying state inside them breaks Mobx's caching and leads to unpredictable behavior.
- Why not have Boxes fetch positions from the store? This creates tight coupling between Boxes and the store, making your code harder to test and maintain. Directly updating positions via actions is cleaner.
- Reaction vs Autorun? We use
reactionhere because it only runs when specific dependencies change (sort/filter/visible boxes), whereasautorunwould run on any observable change in the store. This is more performant.
Performance Tips for Large Datasets
- If your force-directed layout is computationally heavy, consider offloading it to a web worker to avoid blocking the main thread.
- Use virtualization libraries like
react-windowto render only the Boxes visible in the viewport—this drastically reduces DOM nodes for large lists.
内容的提问来源于stack exchange,提问作者Flavio

