Side effects that automatically run when observable state changes, providing different patterns for responding to state changes. Reactions are the bridge between the reactive MobX data and imperative side effects.
Automatically runs a function when any observable values it accesses change.
/**
* Runs a function immediately and re-runs when observables change
* @param fn - Function to run automatically
* @param options - Configuration options
* @returns Disposer function to stop the autorun
*/
function autorun(
fn: (reaction: IReactionPublic) => void,
options?: IAutorunOptions
): IReactionDisposer;Usage Examples:
import { observable, autorun } from "mobx";
const person = observable({
name: "Alice",
age: 25
});
// Basic autorun - runs immediately and on changes
const dispose = autorun(() => {
console.log(`Person: ${person.name}, Age: ${person.age}`);
});
// Logs: "Person: Alice, Age: 25"
person.name = "Bob";
// Logs: "Person: Bob, Age: 25"
person.age = 26;
// Logs: "Person: Bob, Age: 26"
// Stop the autorun
dispose();
person.name = "Charlie"; // No longer logsAutorun supports various configuration options for controlling its behavior.
interface IAutorunOptions {
/** Delay in milliseconds before running (debouncing) */
delay?: number;
/** Debug name for the reaction */
name?: string;
/** Warn if the reaction doesn't track any observables */
requiresObservable?: boolean;
/** Custom scheduler for when to run the reaction */
scheduler?: (callback: () => void) => any;
/** Error handler for exceptions in the reaction */
onError?: (error: any) => void;
/** AbortSignal for cancelling the reaction */
signal?: GenericAbortSignal;
}Usage Examples:
import { observable, autorun } from "mobx";
const state = observable({ count: 0 });
// Debounced autorun
const dispose1 = autorun(() => {
console.log("Count:", state.count);
}, {
delay: 100, // Wait 100ms after last change
name: "count-logger"
});
// Autorun with error handling
const dispose2 = autorun(() => {
if (state.count < 0) {
throw new Error("Count cannot be negative");
}
console.log("Valid count:", state.count);
}, {
onError: (error) => {
console.error("Autorun error:", error.message);
}
});
// Autorun with custom scheduler
const dispose3 = autorun(() => {
console.log("Scheduled count:", state.count);
}, {
scheduler: (callback) => {
// Run on next animation frame
requestAnimationFrame(callback);
}
});More precise reaction that separates data expression from side effect, only running the effect when the data expression returns a different value.
/**
* Runs effect when data expression returns a new value
* @param expression - Function that returns tracked data
* @param effect - Side effect function called with new and old values
* @param options - Configuration options
* @returns Disposer function to stop the reaction
*/
function reaction<T>(
expression: (reaction: IReactionPublic) => T,
effect: (value: T, previousValue: T, reaction: IReactionPublic) => void,
options?: IReactionOptions<T>
): IReactionDisposer;Usage Examples:
import { observable, reaction } from "mobx";
const person = observable({
firstName: "Alice",
lastName: "Smith",
age: 25
});
// React only to name changes, not age
const dispose = reaction(
() => `${person.firstName} ${person.lastName}`, // Data expression
(name, previousName) => { // Effect
console.log(`Name changed from "${previousName}" to "${name}"`);
}
);
person.age = 26; // No effect - age not tracked in expression
person.firstName = "Bob"; // Logs: 'Name changed from "Alice Smith" to "Bob Smith"'
person.lastName = "Jones"; // Logs: 'Name changed from "Bob Smith" to "Bob Jones"'
dispose();Reaction supports configuration options similar to autorun.
interface IReactionOptions<T> {
/** Delay in milliseconds before running effect */
delay?: number;
/** Debug name for the reaction */
name?: string;
/** Custom equality comparer for detecting changes */
equals?: IEqualsComparer<T>;
/** Warn if expression doesn't track observables */
requiresObservable?: boolean;
/** Custom scheduler for when to run the effect */
scheduler?: (callback: () => void) => any;
/** Error handler for exceptions */
onError?: (error: any) => void;
/** Run effect immediately with initial value */
fireImmediately?: boolean;
}Usage Examples:
import { observable, reaction, comparer } from "mobx";
const store = observable({
items: [],
filter: "all"
});
// Reaction with structural equality
const dispose1 = reaction(
() => store.items.filter(item =>
store.filter === "all" || item.category === store.filter
),
(filteredItems, prevItems) => {
console.log(`Filtered items changed: ${filteredItems.length} items`);
},
{
equals: comparer.structural, // Only react to structural changes
fireImmediately: true, // Run immediately with initial value
name: "filter-reaction"
}
);
// Reaction with custom equality
const dispose2 = reaction(
() => ({ count: store.items.length, filter: store.filter }),
(stats, prevStats) => {
console.log("Stats changed:", stats);
},
{
equals: (a, b) => a.count === b.count && a.filter === b.filter
}
);Waits for a predicate to become true, then runs an effect (once) or returns a promise.
/**
* Waits for predicate to become true, then runs effect once
* @param predicate - Function that returns boolean
* @param effect - Effect to run when predicate becomes true
* @param options - Configuration options
* @returns Disposer function
*/
function when(
predicate: () => boolean,
effect?: () => void,
options?: IWhenOptions
): IReactionDisposer;
/**
* Returns promise that resolves when predicate becomes true
* @param predicate - Function that returns boolean
* @param options - Configuration options
* @returns Promise that resolves when predicate is true
*/
function when(
predicate: () => boolean,
options?: IWhenOptions
): Promise<void>;Usage Examples:
import { observable, when, autorun } from "mobx";
const state = observable({
loading: false,
data: null,
user: null
});
// Wait for data to load, then do something
const dispose1 = when(
() => !state.loading && state.data !== null,
() => {
console.log("Data loaded:", state.data);
// Process data...
}
);
// Promise-based usage
async function waitForUser() {
await when(() => state.user !== null);
console.log("User is now available:", state.user.name);
}
// Wait with timeout
const waitForUserWithTimeout = when(
() => state.user !== null,
{
timeout: 5000,
onError: (error) => {
console.error("Timeout waiting for user");
}
}
);
waitForUserWithTimeout.then(() => {
console.log("User loaded within timeout");
}).catch(error => {
console.error("Failed to load user:", error);
});Configuration options for when reactions.
interface IWhenOptions {
/** Debug name for the reaction */
name?: string;
/** Timeout in milliseconds */
timeout?: number;
/** Error handler */
onError?: (error: any) => void;
/** AbortSignal for cancellation */
signal?: GenericAbortSignal;
}All reactions return a disposer function for cleanup.
interface IReactionDisposer {
/** Stop the reaction */
(): void;
/** Access to the underlying reaction object */
$mobx: Reaction;
}Usage Examples:
import { observable, autorun, reaction, when } from "mobx";
const state = observable({ count: 0 });
const disposers = [];
// Collect disposers
disposers.push(
autorun(() => console.log("Autorun:", state.count)),
reaction(() => state.count, (count) => console.log("Reaction:", count)),
when(() => state.count > 5, () => console.log("Count exceeded 5"))
);
// Clean up all reactions
function cleanup() {
disposers.forEach(dispose => dispose());
disposers.length = 0;
}
// Clean up on page unload, component unmount, etc.
window.addEventListener("beforeunload", cleanup);Low-level reaction class for advanced use cases.
class Reaction implements IReactionPublic {
constructor(name?: string, onInvalidate?: () => void);
/** Track observables accessed in the function */
track(fn: () => void): void;
/** Dispose the reaction */
dispose(): void;
/** Get current reaction state */
isDisposed: boolean;
/** Reaction name for debugging */
name: string;
}
interface IReactionPublic {
name: string;
dispose(): void;
trace(enterBreakPoint?: boolean): void;
}Usage Examples:
import { observable, Reaction } from "mobx";
const state = observable({ count: 0, name: "test" });
// Custom reaction implementation
const myReaction = new Reaction("MyCustomReaction", () => {
console.log("Something changed, scheduling update...");
// Schedule custom update logic
setTimeout(() => {
myReaction.track(() => {
// This will track new dependencies
console.log("Current state:", state.count, state.name);
});
}, 0);
});
// Initial tracking
myReaction.track(() => {
console.log("Initial:", state.count);
});
// Changes will trigger the onInvalidate callback
state.count = 1;
state.count = 2;
// Clean up
setTimeout(() => {
myReaction.dispose();
}, 1000);Reactions that only run under certain conditions.
import { observable, reaction, autorun } from "mobx";
const state = observable({
isEnabled: true,
data: "initial"
});
// Conditional autorun
const dispose = autorun(() => {
if (!state.isEnabled) return;
console.log("Processing:", state.data);
// Only runs when enabled AND data changes
});
// Conditional reaction
const dispose2 = reaction(
() => state.isEnabled ? state.data : null,
(data) => {
if (data) {
console.log("Data changed while enabled:", data);
}
}
);Using delay option for performance optimization.
import { observable, autorun } from "mobx";
const searchState = observable({
query: "",
results: []
});
// Debounced search
const dispose = autorun(() => {
if (searchState.query.length > 2) {
performSearch(searchState.query);
}
}, {
delay: 300, // Wait 300ms after last change
name: "search-reaction"
});
function performSearch(query) {
console.log("Searching for:", query);
// Perform actual search...
}Proper error handling prevents reactions from being disposed on errors.
import { observable, autorun, configure } from "mobx";
// Configure global error handling
configure({
disableErrorBoundaries: false
});
const state = observable({
data: null,
shouldThrow: false
});
const dispose = autorun(() => {
if (state.shouldThrow) {
throw new Error("Intentional error");
}
console.log("Data:", state.data);
}, {
onError: (error) => {
console.error("Caught error in reaction:", error.message);
// Reaction continues to work after error
}
});
// This won't break the reaction
state.shouldThrow = true;
state.shouldThrow = false;
state.data = "new data"; // Still logs