or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

actions.mdcomputed.mdconfiguration.mdindex.mdobservables.mdreactions.mdutilities.md
tile.json

actions.mddocs/

Actions and Flows

Functions that modify observable state, providing performance optimizations through batching and supporting asynchronous operations through generators. Actions ensure that all state changes are tracked and batched for optimal performance.

Capabilities

Action Function

Wraps functions as actions that modify observable state, providing performance optimizations and debugging benefits.

/**
 * Creates an action that can modify observable state
 * @param fn - Function to wrap as action
 * @returns Action-wrapped function
 */
function action<T extends Function>(fn: T): T;

/**
 * Creates a named action for better debugging
 * @param name - Debug name for the action
 * @param fn - Function to wrap as action  
 * @returns Action-wrapped function
 */
function action<T extends Function>(name: string, fn: T): T;

Usage Examples:

import { observable, action, autorun } from "mobx";

const state = observable({
  count: 0,
  items: []
});

// Simple action
const increment = action(() => {
  state.count++;
});

// Named action for better debugging
const addItem = action("addItem", (item) => {
  state.items.push(item);
  state.count = state.items.length;
});

// Multiple state changes are batched in actions
const batchUpdate = action(() => {
  state.count = 0;
  state.items.clear();
  state.items.push("first", "second", "third");  
  state.count = state.items.length;
  // Only triggers reactions once at the end
});

autorun(() => {
  console.log(`Count: ${state.count}, Items: ${state.items.length}`);
});

increment(); // Logs once
batchUpdate(); // Logs once despite multiple changes

Action Decorator

Decorator for marking class methods as actions.

/**
 * Decorator interface for actions in classes
 * Can be used as @action, @action(name), or @action.bound
 */
interface IActionFactory extends Annotation, PropertyDecorator {
  /** Named action decorator */
  (customName: string): PropertyDecorator & Annotation;
  /** Auto-bound action decorator */
  bound: Annotation & PropertyDecorator;
}

declare const action: IActionFactory;

Usage Examples:

import { makeObservable, observable, action } from "mobx";

class TodoStore {
  todos = [];

  constructor() {
    makeObservable(this, {
      todos: observable,
      addTodo: action,
      removeTodo: action.bound, // Auto-bound method
      clearCompleted: action
    });
  }

  addTodo(text) {
    this.todos.push({ 
      id: Date.now(), 
      text, 
      completed: false 
    });
  }

  removeTodo = action("removeTodo", (id) => {
    const index = this.todos.findIndex(todo => todo.id === id);
    if (index !== -1) {
      this.todos.splice(index, 1);
    }
  });

  clearCompleted() {
    this.todos = this.todos.filter(todo => !todo.completed);
  }
}

// With automatic detection
class CounterAuto {
  count = 0;

  constructor() {
    makeAutoObservable(this); // Automatically detects methods as actions
  }

  increment() {
    this.count++;
  }

  decrement() {
    this.count--;
  }

  reset() {
    this.count = 0;
  }
}

Run In Action

Runs code blocks as actions without requiring function wrapping.

/**
 * Runs a function as an action
 * @param fn - Function to run as action
 * @returns Result of the function
 */
function runInAction<T>(fn: () => T): T;

/**
 * Runs a named function as an action
 * @param name - Debug name for the action
 * @param fn - Function to run as action
 * @returns Result of the function
 */
function runInAction<T>(name: string, fn: () => T): T;

Usage Examples:

import { observable, runInAction, autorun } from "mobx";

const state = observable({
  loading: false,
  data: null,
  error: null
});

// Direct usage for immediate actions
async function fetchData(url) {
  runInAction(() => {
    state.loading = true;
    state.error = null;
  });

  try {
    const response = await fetch(url);
    const data = await response.json();
    
    runInAction("fetch success", () => {
      state.data = data;
      state.loading = false;
    });
  } catch (error) {
    runInAction("fetch error", () => {
      state.error = error.message;
      state.loading = false;
    });
  }
}

// Useful for event handlers
const handleClick = (event) => {
  runInAction(() => {
    state.data = null;
    state.error = null;
    state.loading = !state.loading;
  });
};

Flow

Creates async actions using generator functions, providing a clean way to handle asynchronous state changes.

/**
 * Creates an async action using generator functions
 * @param generator - Generator function that yields promises
 * @returns Function that returns a Promise
 */
function flow<R, Args extends any[]>(
  generator: (...args: Args) => Generator<any, R, any>
): (...args: Args) => Promise<R>;

Usage Examples:

import { observable, flow, autorun } from "mobx";

const state = observable({
  loading: false,
  users: [],
  error: null
});

// Flow for async operations
const fetchUsers = flow(function* () {
  state.loading = true;
  state.error = null;

  try {
    // Yield promises - MobX handles the async flow
    const response = yield fetch("/api/users");
    const users = yield response.json();
    
    // State changes after yield are automatically wrapped in actions
    state.users = users;
    state.loading = false;
    
    return users;
  } catch (error) {
    state.error = error.message;
    state.loading = false;
    throw error;
  }
});

// Flow with parameters
const createUser = flow(function* (userData) {
  state.loading = true;
  
  try {
    const response = yield fetch("/api/users", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(userData)
    });
    
    const newUser = yield response.json();
    state.users.push(newUser);
    state.loading = false;
    
    return newUser;
  } catch (error) {
    state.error = error.message;
    state.loading = false;
    throw error;
  }
});

// Usage
fetchUsers().then(users => {
  console.log("Loaded users:", users.length);
});

createUser({ name: "Alice", email: "alice@example.com" });

Flow Result

Unwraps flow results for proper TypeScript typing and cancellation support.

/**
 * Unwraps the result of a flow for proper typing
 * @param flowResult - Result from calling a flow
 * @returns Promise with proper typing
 */
function flowResult<T>(flowResult: T): T extends Promise<infer R> ? Promise<R> : T;

Usage Examples:

import { flow, flowResult } from "mobx";

const myFlow = flow(function* () {
  yield new Promise(resolve => setTimeout(resolve, 1000));
  return "completed";
});

// Without flowResult - TypeScript sees generic return type
const result1 = myFlow(); // Type: any

// With flowResult - TypeScript sees Promise<string>
const result2 = flowResult(myFlow()); // Type: Promise<string>

result2.then(value => {
  console.log(value); // "completed" with proper typing
});

Flow Cancellation

Flows can be cancelled, providing clean cancellation semantics for async operations.

/**
 * Error thrown when a flow is cancelled
 */
class FlowCancellationError extends Error {
  name: "FlowCancellationError";
}

/**
 * Checks if an error is a flow cancellation error
 * @param error - Error to check
 * @returns True if error is from flow cancellation
 */
function isFlowCancellationError(error: any): error is FlowCancellationError;

Usage Examples:

import { 
  flow, 
  flowResult, 
  FlowCancellationError, 
  isFlowCancellationError 
} from "mobx";

const longRunningFlow = flow(function* () {
  for (let i = 0; i < 10; i++) {
    yield new Promise(resolve => setTimeout(resolve, 1000));
    console.log(`Step ${i + 1}`);
  }
  return "completed";
});

// Start the flow
const promise = flowResult(longRunningFlow());

// Cancel after 3 seconds
setTimeout(() => {
  promise.cancel();
}, 3000);

// Handle cancellation
promise.catch(error => {
  if (isFlowCancellationError(error)) {
    console.log("Flow was cancelled");
  } else {
    console.error("Flow failed:", error);
  }
});

Action Utilities

Is Action

Checks if a function is wrapped as an action.

/**
 * Checks if a function is an action
 * @param fn - Function to check
 * @returns True if function is an action
 */
function isAction(fn: any): boolean;

Is Flow

Checks if a function is a flow.

/**
 * Checks if a function is a flow
 * @param fn - Function to check  
 * @returns True if function is a flow
 */
function isFlow(fn: any): boolean;

Usage Examples:

import { action, flow, isAction, isFlow } from "mobx";

const normalFunction = () => {};
const actionFunction = action(() => {});
const flowFunction = flow(function* () {});

console.log(isAction(normalFunction));   // false
console.log(isAction(actionFunction));   // true
console.log(isAction(flowFunction));     // false

console.log(isFlow(normalFunction));     // false
console.log(isFlow(actionFunction));     // false  
console.log(isFlow(flowFunction));       // true

Advanced Action Patterns

Action Context Information

Actions provide context information for debugging and introspection.

interface IActionRunInfo {
  name: string;
  arguments: any[];
  context: any;
}

/**
 * Low-level action tracking functions
 */
function _startAction(name: string, scope: any, args: any[]): IActionRunInfo;
function _endAction(runInfo: IActionRunInfo): void;

Conditional Actions

Actions that only run under certain conditions.

import { observable, action, configure } from "mobx";

// Configure strict mode
configure({ enforceActions: "always" });

const state = observable({ count: 0 });

const conditionalUpdate = action((newValue) => {
  if (newValue > state.count) {
    state.count = newValue;
  }
});

// This would throw in strict mode without action wrapper
// state.count = 5; // Error!

conditionalUpdate(5); // OK

Async Action Patterns

Different patterns for handling async operations with actions.

import { observable, action, runInAction, flow } from "mobx";

const store = observable({
  data: null,
  loading: false,
  error: null
});

// Pattern 1: Multiple runInAction calls
const fetchData1 = async () => {
  runInAction(() => {
    store.loading = true;
    store.error = null;
  });

  try {
    const data = await api.fetchData();
    runInAction(() => {
      store.data = data;
      store.loading = false;
    });
  } catch (error) {
    runInAction(() => {
      store.error = error;
      store.loading = false;
    });
  }
};

// Pattern 2: Using flow (recommended)
const fetchData2 = flow(function* () {
  store.loading = true;
  store.error = null;

  try {
    const data = yield api.fetchData();
    store.data = data;
    store.loading = false;
  } catch (error) {
    store.error = error;
    store.loading = false;
  }
});