CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-mobx-react-lite

Lightweight React bindings for MobX based on React 16.8+ and Hooks

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

local-state.mddocs/

Local Observable State

Hooks for creating and managing observable state within React components. These hooks provide component-local observable state that integrates seamlessly with the React component lifecycle and MobX reactivity system.

Capabilities

useLocalObservable Hook

Creates an observable object using the provided initializer function. This is the recommended way to create local observable state in functional components.

/**
 * Creates an observable object with the given properties, methods and computed values
 * @param initializer - Function that returns the initial state object
 * @param annotations - Optional MobX annotations for fine-tuned observability control
 * @returns Observable state object with stable reference
 */
function useLocalObservable<TStore extends Record<string, any>>(
  initializer: () => TStore,
  annotations?: AnnotationsMap<TStore, never>
): TStore;

Usage Examples:

import { observer, useLocalObservable } from "mobx-react-lite";
import { computed, action } from "mobx";

// Basic observable state
const Counter = observer(() => {
  const store = useLocalObservable(() => ({
    count: 0,
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
    get doubled() {
      return this.count * 2;
    }
  }));

  return (
    <div>
      <p>Count: {store.count}</p>
      <p>Doubled: {store.doubled}</p>
      <button onClick={store.increment}>+</button>
      <button onClick={store.decrement}>-</button>
    </div>
  );
});

// With explicit annotations
const TodoList = observer(() => {
  const store = useLocalObservable(
    () => ({
      todos: [],
      filter: 'all',
      addTodo(text: string) {
        this.todos.push({ id: Date.now(), text, completed: false });
      },
      toggleTodo(id: number) {
        const todo = this.todos.find(t => t.id === id);
        if (todo) todo.completed = !todo.completed;
      },
      get visibleTodos() {
        switch (this.filter) {
          case 'active': return this.todos.filter(t => !t.completed);
          case 'completed': return this.todos.filter(t => t.completed);
          default: return this.todos;
        }
      }
    }),
    {
      addTodo: action,
      toggleTodo: action,
      visibleTodos: computed
    }
  );

  return (
    <div>
      <input 
        onKeyDown={(e) => {
          if (e.key === 'Enter') {
            store.addTodo(e.currentTarget.value);
            e.currentTarget.value = '';
          }
        }}
      />
      {store.visibleTodos.map(todo => (
        <div key={todo.id} onClick={() => store.toggleTodo(todo.id)}>
          {todo.completed ? '✓' : '○'} {todo.text}
        </div>
      ))}
    </div>
  );
});

// Integration with props
import { useEffect } from "react";

interface User {
  name: string;
  email: string;
}

// Mock API function
declare function fetchUser(userId: string): Promise<User>;

const UserProfile = observer(({ userId }: { userId: string }) => {
  const store = useLocalObservable(() => ({
    user: null as User | null,
    loading: true,
    error: null as string | null,
    
    async loadUser() {
      this.loading = true;
      this.error = null;
      try {
        this.user = await fetchUser(userId);
      } catch (err) {
        this.error = err.message;
      } finally {
        this.loading = false;
      }
    }
  }));

  useEffect(() => {
    store.loadUser();
  }, [userId, store]);

  if (store.loading) return <div>Loading...</div>;
  if (store.error) return <div>Error: {store.error}</div>;
  if (!store.user) return <div>User not found</div>;

  return (
    <div>
      <h1>{store.user.name}</h1>
      <p>{store.user.email}</p>
    </div>
  );
});

useLocalStore Hook (Deprecated)

Creates an observable store using an initializer function. This hook is deprecated in favor of useLocalObservable.

/**
 * @deprecated Use useLocalObservable instead
 * Creates local observable state with optional source synchronization
 * @param initializer - Function that returns the initial store object
 * @param current - Optional source object to synchronize with
 * @returns Observable store object
 */
function useLocalStore<TStore extends Record<string, any>>(
  initializer: () => TStore
): TStore;

function useLocalStore<TStore extends Record<string, any>, TSource extends object>(
  initializer: (source: TSource) => TStore,
  current: TSource
): TStore;

Migration Example:

// Old approach (deprecated)
const store = useLocalStore(() => ({
  count: 0,
  increment() { this.count++; }
}));

// New approach (recommended)
const store = useLocalObservable(() => ({
  count: 0,
  increment() { this.count++; }
}));

useAsObservableSource Hook (Deprecated)

Converts any set of values into an observable object with stable reference. This hook is deprecated due to anti-pattern of updating observables during rendering.

/**
 * @deprecated Use useEffect to synchronize non-observable values instead
 * Converts values into an observable object with stable reference
 * @param current - Source object to make observable
 * @returns Observable version of the source object
 */
function useAsObservableSource<TSource extends object>(current: TSource): TSource;

Migration Example:

// Old approach (deprecated)
function Measurement({ unit }: { unit: string }) {
  const observableProps = useAsObservableSource({ unit });
  const state = useLocalStore(() => ({
    length: 0,
    get lengthWithUnit() {
      return observableProps.unit === "inch"
        ? `${this.length / 2.54} inch`
        : `${this.length} cm`;
    }
  }));

  return <h1>{state.lengthWithUnit}</h1>;
}

// New approach (recommended)
import { useEffect } from "react";

function Measurement({ unit }: { unit: string }) {
  const state = useLocalObservable(() => ({
    length: 0,
    unit: unit,
    get lengthWithUnit() {
      return this.unit === "inch"
        ? `${this.length / 2.54} inch`
        : `${this.length} cm`;
    }
  }));

  // Sync prop changes to observable state
  useEffect(() => {
    state.unit = unit;
  }, [unit, state]);

  return <h1>{state.lengthWithUnit}</h1>;
}

Important Notes

Best Practices

  • Use initializer functions: Always pass a function to create fresh state for each component instance
  • Avoid closures: Don't capture variables from component scope in the initializer to prevent stale closures
  • Stable references: The returned observable object has a stable reference throughout the component lifecycle
  • Auto-binding: Methods are automatically bound to the observable object (autoBind: true)
  • Computed values: Use getter functions for derived state that should be cached and reactive

Integration with React

  • Component lifecycle: Observable state is created once per component instance and disposed on unmount
  • Effect synchronization: Use useEffect to synchronize props or external state with observable state
  • Error boundaries: Exceptions in observable methods are properly caught by React error boundaries
  • Development tools: Observable state is debuggable through MobX developer tools

Performance Considerations

  • Lazy evaluation: Computed values are only recalculated when their dependencies change
  • Automatic batching: Multiple state changes in the same tick are batched for optimal rendering
  • Memory management: Reactions and computed values are automatically disposed when component unmounts
  • Shallow observation: Only the direct properties of the returned object are observable by default

Types

// MobX annotations from mobx package
type Annotation = {
  annotationType_: string;
  make_(adm: any, key: PropertyKey, descriptor: PropertyDescriptor, source: object): any;
  extend_(adm: any, key: PropertyKey, descriptor: PropertyDescriptor, proxyTrap: boolean): boolean | null;
  options_?: any;
};

type AnnotationMapEntry = Annotation | true | false;

type AnnotationsMap<T, AdditionalKeys extends PropertyKey> = {
  [K in keyof T | AdditionalKeys]?: AnnotationMapEntry;
};

docs

advanced.md

index.md

local-state.md

observers.md

static-rendering.md

tile.json