What is MobX?
MobX is a battle-tested, lightweight state management library for JavaScript applications. It makes state management transparent and predictable by applying functional reactive programming principles behind the scenes. The core idea is deceptively simple: anything that can be derived from the application state, should be derived automatically.
Unlike Redux's single-store, action-reducer pattern, MobX allows you to work with multiple observable stores and automatically tracks which parts of your state are used by which components. This eliminates the need for manual subscription management, selector memoization, or boilerplate-heavy action dispatching.
At its heart, MobX consists of four fundamental pillars:
- Observable State β Data that MobX tracks for changes
- Actions β Functions that mutate observable state
- Computed Values β Values derived from state that update automatically
- Reactions β Side effects that run when observable data changes
MobX works beautifully with React (via mobx-react or mobx-react-lite), but it's framework-agnostic and can be used with Vue, Angular, or even vanilla JavaScript.
Why MobX Matters
π Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →In modern frontend development, managing complex client-side state is one of the hardest problems. As applications grow, shared mutable state, asynchronous updates, and derived data create a tangled web of dependencies. MobX addresses these challenges through several key advantages:
- Zero Boilerplate β No reducers, action types, or switch statements. You write plain JavaScript classes or objects.
- Automatic Efficiency β MobX tracks exactly which observables are read during a computation and only re-evaluates when those specific dependencies change. No manual
useMemoorReact.memogymnastics. - Fine-Grained Reactivity β Only components that read changed observables re-render. Your entire component tree isn't force-updated.
- Mental Model Simplicity β You think in terms of "what data do I have, what do I want to derive from it, and what should happen when it changes."
- TypeScript First β Excellent TypeScript support with minimal type annotation overhead.
- Battle-Tested β Used in production by companies like Microsoft, Coinbase, and countless startups.
The difference becomes stark when comparing a typical MobX store to an equivalent Redux setup. MobX lets you focus on business logic rather than plumbing.
Installation and Setup
Let's start by installing the necessary packages. For a React project, you'll typically want mobx and mobx-react-lite:
npm install mobx mobx-react-lite
# or
yarn add mobx mobx-react-lite
If you're using class components or need decorator support (older codebases), you may also install mobx-react with decorator support, but modern MobX (v6+) encourages the non-decorator API. Throughout this tutorial, we'll use the modern, non-decorator syntax.
For TypeScript users, no additional type packages are neededβMobX ships with its own type definitions.
npm install mobx mobx-react-lite typescript @types/react
Level 1: Your First Observable Store
Creating Observable State
The most fundamental operation in MobX is making data observable. Let's build a simple counter store:
import { makeAutoObservable, makeObservable, observable, action, computed } from "mobx";
// Approach A: makeAutoObservable (recommended for beginners)
class CounterStore {
count = 0;
multiplier = 2;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++;
}
decrement() {
this.count--;
}
get doubledCount() {
return this.count * this.multiplier;
}
reset() {
this.count = 0;
}
}
// Create a single instance (singleton store pattern)
const counterStore = new CounterStore();
export default counterStore;
makeAutoObservable automatically converts all properties to observable, all getters to computed, and all methods to action. This is the fastest way to get started and works perfectly for most use cases.
Connecting to React
To make a React component reactive to MobX state, wrap it with observer:
import React from "react";
import { observer } from "mobx-react-lite";
import counterStore from "./stores/CounterStore";
const Counter = observer(() => {
return (
<div>
<h1>Count: {counterStore.count}</h1>
<h2>Doubled: {counterStore.doubledCount}</h2>
<button onClick={() => counterStore.increment()}>+</button>
<button onClick={() => counterStore.decrement()}>-</button>
<button onClick={() => counterStore.reset()}>Reset</button>
</div>
);
});
export default Counter;
The observer HOC (or wrapper function) converts your component into a reactive component that automatically re-renders when any observable value it reads during render is changed. Notice there's no useSelector, no connect, no mapStateToPropsβyou simply access properties directly.
How It Works Under the Hood
When the component renders, MobX tracks which observables are accessed. This creates a dependency graph. When counterStore.count changes, only components that read count (or a computed value derived from count) will re-render. Components reading unrelated observables remain untouched.
// Inside MobX's tracking system (simplified mental model):
// 1. Observer component renders β tracking starts
// 2. counterStore.count is read β dependency recorded
// 3. counterStore.doubledCount is read β dependency recorded (transitively depends on count)
// 4. Tracking ends
// 5. Later: count changes β notification fires β component re-renders
Level 2: Understanding the Core API
Explicit Annotations with makeObservable
While makeAutoObservable is convenient, production code often requires explicit control. Use makeObservable to specify exactly which members are observable, actions, or computed:
import { makeObservable, observable, action, computed, flow } from "mobx";
class TodoStore {
todos = [];
filter = "all"; // 'all' | 'active' | 'completed'
isLoading = false;
constructor() {
makeObservable(this, {
todos: observable,
filter: observable,
isLoading: observable,
// All getters become computed
filteredTodos: computed,
activeTodoCount: computed,
completedTodoCount: computed,
// All methods that mutate state become actions
addTodo: action,
toggleTodo: action,
removeTodo: action,
setFilter: action,
// Async actions use flow
fetchTodos: flow
});
}
get filteredTodos() {
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;
}
}
get activeTodoCount() {
return this.todos.filter(t => !t.completed).length;
}
get completedTodoCount() {
return this.todos.filter(t => t.completed).length;
}
addTodo(text) {
this.todos.push({
id: Date.now(),
text,
completed: false,
createdAt: new Date()
});
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
removeTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
}
setFilter(filter) {
this.filter = filter;
}
// Async action using generator function
*fetchTodos() {
this.isLoading = true;
try {
const response = yield fetch("https://api.example.com/todos");
const data = yield response.json();
this.todos = data.map(item => ({
id: item.id,
text: item.title,
completed: item.completed,
createdAt: new Date(item.createdAt)
}));
} catch (error) {
console.error("Failed to fetch todos:", error);
} finally {
this.isLoading = false;
}
}
}
The Three Annotation Types Deep Dive
Observable β These are the tracked properties. MobX supports observable, observable.ref (shallow tracking), observable.shallow (one level deep), and observable.struct (structural comparison). Choose the right one for your data structure:
import { observable } from "mobx";
class ConfigStore {
// Deep observable (default) - tracks every nested property
settings = observable({
theme: "light",
notifications: { email: true, push: false }
});
// Reference only - only tracks if the reference changes
externalData = observable.ref({ heavy: "payload" });
// Shallow - tracks direct properties but not nested ones
shallowMap = observable.shallow(new Map());
// Struct - treats value as immutable, compares structurally
point = observable.struct({ x: 0, y: 0 });
}
Action β Functions that modify state. By default in strict mode (enabled via configure({ enforceActions: "observed" })), state mutations outside actions throw errors. Actions batch mutations and notify observers only after completion:
import { action, runInAction } from "mobx";
class FormStore {
name = "";
email = "";
errors = {};
// Named action with explicit annotation
updateField(field, value) {
this[field] = value;
}
// Multiple mutations batched together
submitForm() {
// All mutations inside an action are batched
// Observers are notified once after all mutations complete
this.validate();
if (Object.keys(this.errors).length === 0) {
this.sendToServer();
}
}
validate() {
this.errors = {};
if (!this.name) this.errors.name = "Name is required";
if (!this.email.includes("@")) this.errors.email = "Invalid email";
}
// For wrapping non-action code (callbacks, promises)
handleExternalUpdate(data) {
runInAction(() => {
this.name = data.name;
this.email = data.email;
});
}
}
Computed β Values derived from state. They are lazy (only evaluated when accessed), cached (result reused until dependencies change), and pure (must not modify state):
import { computed, makeObservable, observable } from "mobx";
class ShoppingCart {
items = [];
constructor() {
makeObservable(this, {
items: observable,
totalPrice: computed,
itemCount: computed,
hasDiscount: computed,
discountAmount: computed,
finalPrice: computed
});
}
get totalPrice() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
get itemCount() {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
get hasDiscount() {
return this.totalPrice > 100;
}
get discountAmount() {
return this.hasDiscount ? this.totalPrice * 0.1 : 0;
}
get finalPrice() {
// Computed values can depend on other computed values
// MobX builds the dependency chain automatically
return this.totalPrice - this.discountAmount;
}
}
// Usage: computed values behave like regular property access
const cart = new ShoppingCart();
cart.items.push({ name: "Book", price: 29.99, quantity: 2 });
console.log(cart.totalPrice); // 59.98
// Add more items to trigger discount
cart.items.push({ name: "Headphones", price: 89.99, quantity: 1 });
console.log(cart.finalPrice); // 107.972 (with discount applied)
Level 3: Reactions and Side Effects
While computed values derive new values from state, reactions produce side effects. MobX provides three main reaction primitives:
autorun
autorun runs a function immediately and then re-runs it whenever any observable it reads changes. Perfect for logging, debugging, or imperative DOM updates:
import { autorun, observable } from "mobx";
const temperature = observable.box(25); // observable.box for primitive values
const disposer = autorun(() => {
console.log(`Current temperature: ${temperature.get()}Β°C`);
// This runs immediately and then whenever temperature changes
});
temperature.set(30); // Logs: Current temperature: 30Β°C
temperature.set(18); // Logs: Current temperature: 18Β°C
// Always dispose when no longer needed
disposer();
reaction
reaction separates the tracking function from the effect function. The first argument tracks dependencies (like a computed), and the second runs only when those dependencies change. This gives you precise control:
import { reaction, makeAutoObservable } from "mobx";
class DocumentStore {
title = "Untitled";
content = "";
lastSaved = null;
constructor() {
makeAutoObservable(this);
}
}
const docStore = new DocumentStore();
// Track specific observables, run effect only when they change
const disposer = reaction(
// Tracking function - returns the data to watch
() => ({
title: docStore.title,
content: docStore.content
}),
// Effect function - receives the current and previous tracked values
({ title, content }, prev) => {
console.log(`Document changed from "${prev.title}" to "${title}"`);
// Trigger auto-save, update preview, etc.
scheduleAutoSave(title, content);
},
{
// Optional: don't run immediately, only on changes
fireImmediately: false,
// Optional: delay execution (debounce)
delay: 1000
}
);
docStore.title = "My Document"; // Effect runs after 1 second (debounced)
when
when runs an effect once when a condition becomes true and then automatically disposes itself:
import { when, makeAutoObservable } from "mobx";
class GameStore {
score = 0;
level = 1;
constructor() {
makeAutoObservable(this);
}
}
const gameStore = new GameStore();
// Execute side effect once when condition is met
when(
() => gameStore.score >= 1000 && gameStore.level === 1,
() => {
console.log("Level up! Reached 1000 points.");
gameStore.level = 2;
// This reaction disposes itself after running once
}
);
// Simulate gameplay
for (let i = 0; i < 50; i++) {
gameStore.score += 20;
}
// At some point score reaches 1000 β level up triggers
Custom Reactions with Observer
In React, the observer wrapper itself is a reactionβit's essentially an autorun that re-renders the component when dependencies change. You rarely need explicit reactions in React apps because components serve this purpose:
import { observer } from "mobx-react-lite";
import { useStore } from "./StoreContext";
const NotificationBadge = observer(() => {
const store = useStore();
// Component automatically re-renders when unreadCount changes
return (
<div className="badge">
{store.unreadCount > 0 && <span>{store.unreadCount}</span>}
</div>
);
});
// Equivalent explicit reaction (not needed in React, but shows the pattern):
// const disposer = autorun(() => {
// document.getElementById("badge").textContent = store.unreadCount;
// });
Level 4: Advanced Store Architecture
Store Composition and Context
Real applications need multiple stores that can reference each other. MobX excels at this through straightforward dependency injection:
import { makeAutoObservable } from "mobx";
import { createContext, useContext } from "react";
// UserStore - manages authentication and user data
class UserStore {
user = null;
token = null;
constructor(rootStore) {
this.rootStore = rootStore; // Reference to parent store
makeAutoObservable(this);
}
get isLoggedIn() {
return !!this.token;
}
login(email, password) {
// After successful login
this.token = "auth-token-123";
this.user = { email, name: "John" };
// Cross-store communication via rootStore
this.rootStore.cartStore.loadUserCart();
}
logout() {
this.token = null;
this.user = null;
this.rootStore.resetAll();
}
}
// CartStore - manages shopping cart
class CartStore {
items = [];
constructor(rootStore) {
this.rootStore = rootStore;
makeAutoObservable(this);
}
get itemCount() {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
addItem(product) {
const existing = this.items.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
this.items.push({ ...product, quantity: 1 });
}
}
loadUserCart() {
// Fetch cart from server using user identity from UserStore
// this.rootStore.userStore.user.id
}
}
// UIStore - manages UI state like modals, sidebar, notifications
class UIStore {
sidebarOpen = false;
activeModal = null;
notifications = [];
constructor(rootStore) {
this.rootStore = rootStore;
makeAutoObservable(this);
}
toggleSidebar() {
this.sidebarOpen = !this.sidebarOpen;
}
showNotification(message, type = "info") {
this.notifications.push({ id: Date.now(), message, type });
setTimeout(() => this.dismissNotification(this.notifications[0]?.id), 5000);
}
dismissNotification(id) {
this.notifications = this.notifications.filter(n => n.id !== id);
}
}
// RootStore - aggregates all stores
class RootStore {
constructor() {
this.userStore = new UserStore(this);
this.cartStore = new CartStore(this);
this.uiStore = new UIStore(this);
makeAutoObservable(this);
}
resetAll() {
// Reset logic coordinated across stores
this.cartStore.items = [];
}
}
// React Context for dependency injection
const StoreContext = createContext(null);
export const StoreProvider = ({ children }) => {
const rootStore = new RootStore();
return (
<StoreContext.Provider value={rootStore}>
{children}
</StoreContext.Provider>
);
};
export const useStore = () => {
const store = useContext(StoreContext);
if (!store) throw new Error("useStore must be used within StoreProvider");
return store;
};
This pattern scales beautifully. Stores can communicate through the root store reference, and React components access any store via a single context.
Async Actions with flow
MobX provides flow for handling asynchronous operations using generator functions. This is the recommended approach for complex async flows:
import { flow, makeAutoObservable, runInAction } from "mobx";
class SearchStore {
query = "";
results = [];
isLoading = false;
error = null;
page = 1;
totalPages = 1;
constructor() {
makeAutoObservable(this, {
search: flow,
loadNextPage: flow
});
}
// Generator-based async action
*search(query) {
this.query = query;
this.isLoading = true;
this.error = null;
this.results = [];
this.page = 1;
try {
const response = yield fetch(
`https://api.example.com/search?q=${encodeURIComponent(query)}&page=1`
);
const data = yield response.json();
this.results = data.items;
this.totalPages = data.totalPages;
} catch (err) {
this.error = err.message;
} finally {
this.isLoading = false;
}
}
*loadNextPage() {
if (this.page >= this.totalPages || this.isLoading) return;
this.isLoading = true;
this.page++;
try {
const response = yield fetch(
`https://api.example.com/search?q=${encodeURIComponent(this.query)}&page=${this.page}`
);
const data = yield response.json();
// Append new results
this.results = [...this.results, ...data.items];
} catch (err) {
this.page--; // Rollback page increment on error
this.error = err.message;
} finally {
this.isLoading = false;
}
}
}
// Alternative: async/await with runInAction (if you prefer promises over generators)
class ModernSearchStore {
query = "";
results = [];
isLoading = false;
constructor() {
makeAutoObservable(this);
}
async searchModern(query) {
this.query = query;
this.isLoading = true;
try {
const response = await fetch(`https://api.example.com/search?q=${query}`);
const data = await response.json();
// Must wrap mutations in runInAction for async code
runInAction(() => {
this.results = data.items;
});
} catch (err) {
runInAction(() => {
this.error = err.message;
});
} finally {
runInAction(() => {
this.isLoading = false;
});
}
}
}
The key rule: in async operations, always wrap state mutations in runInAction or use flow. Await breaks the action context, so the code after await is no longer inside an action.
Level 5: Performance Optimization and Best Practices
Granular Observers with React
One of MobX's greatest strengths is granular re-rendering. To maximize this, structure your components to read the minimal set of observables:
import { observer } from "mobx-react-lite";
import { useStore } from "./StoreContext";
// BAD: Large component reads many unrelated observables
const BadDashboard = observer(() => {
const store = useStore();
// Reading user, cart, UI, notifications all in one component
// Any change to any of these triggers re-render of entire dashboard
return (
<div>
<UserPanel user={store.userStore.user} />
<CartPreview items={store.cartStore.items} />
<NotificationList notifications={store.uiStore.notifications} />
</div>
);
});
// GOOD: Split into focused components that each read minimal state
const Dashboard = () => {
return (
<div>
<UserPanel />
<CartPreview />
<NotificationList />
</div>
);
};
const UserPanel = observer(() => {
const store = useStore();
// Only re-renders when user data changes
const { user } = store.userStore;
return <div>{user?.name}</div>;
});
const CartPreview = observer(() => {
const store = useStore();
// Only re-renders when itemCount changes
return <div>Items: {store.cartStore.itemCount}</div>;
});
const NotificationList = observer(() => {
const store = useStore();
// Only re-renders when notifications array changes
return (
<div>
{store.uiStore.notifications.map(n => (
<div key={n.id}>{n.message}</div>
))}
</div>
);
});
Computed Values vs. Reactions
Use computed values whenever you need a derived value that will be consumed by other code. Use reactions only for imperative side effects (logging, DOM manipulation, network requests triggered by state changes):
// GOOD: Use computed for derived data consumed by components
class OrderStore {
lineItems = [];
constructor() {
makeAutoObservable(this);
}
get subtotal() {
return this.lineItems.reduce((sum, item) => sum + item.price * item.qty, 0);
}
get tax() {
return this.subtotal * 0.08; // Computed depends on another computed
}
get total() {
return this.subtotal + this.tax;
}
get summary() {
// Computed values can create complex derived structures
return {
itemCount: this.lineItems.length,
subtotal: this.subtotal,
tax: this.tax,
total: this.total,
timestamp: new Date()
};
}
}
// Use reaction for side effects that don't produce values
const disposer = reaction(
() => orderStore.total,
(total) => {
// Side effect: update analytics, save to localStorage, etc.
analytics.track("order_total_changed", { total });
localStorage.setItem("cart_total", total);
}
);
Action Batching and Transactions
Actions automatically batch updates. Multiple mutations within a single action result in a single notification cycle:
import { action, transaction, runInAction } from "mobx";
class GameEngine {
playerPosition = { x: 0, y: 0 };
playerHealth = 100;
enemies = [];
score = 0;
constructor() {
makeAutoObservable(this);
}
// Single action - all mutations batched
takeDamage(amount) {
// All three mutations complete before observers are notified
this.playerHealth -= amount;
this.updatePlayerAnimation("hurt");
this.checkGameOver();
}
// Use transaction for explicit batching across actions
processTurn() {
transaction(() => {
this.moveEnemies();
this.resolveCombat();
this.spawnItems();
// Observers notified once after transaction completes
});
}
// Non-action code (event handlers, timeouts) use runInAction
handleKeyPress(key) {
// This might be called from a non-MobX context
runInAction(() => {
if (key === "ArrowUp") this.playerPosition.y -= 1;
if (key === "ArrowDown") this.playerPosition.y += 1;
});
}
}
Configuring MobX for Production
MobX provides global configuration options that affect behavior:
import { configure } from "mobx";
// Recommended production configuration
configure({
// Enforce that state mutations only happen inside actions
enforceActions: "observed",
// Use isolates for computed values (safer, prevents cross-observer leaks)
computedRequiresReaction: false,
// Prevent observable values from being created outside makeObservable
reactionRequiresObservable: false,
// Disable MobX warnings in production
observableRequiresReaction: false,
// Use Proxy-based reactivity (modern, performant)
useProxies: "always"
});
Level 6: MobX with TypeScript
MobX and TypeScript work exceptionally well together. Here's how to leverage TypeScript effectively:
import { makeAutoObservable, makeObservable, observable, action, computed, runInAction, flow } from "mobx";
// Define interfaces for your data
interface Todo {
id: string;
text: string;
completed: boolean;
createdAt: Date;
tags: string[];
}
interface TodoFilter {
status: "all" | "active" | "completed";
searchQuery: string;
tagFilter: string | null;
}
class TypedTodoStore {
todos: Todo[] = [];
filter: TodoFilter = {
status: "all",
searchQuery: "",
tagFilter: null
};
isLoading = false;
error: string | null = null;
constructor() {
makeAutoObservable(this);
// TypeScript automatically infers types from the class properties
// No need for explicit generic parameters in most cases
}
// Computed with explicit return type (optional but good practice)
get filteredTodos(): Todo[] {
let result = this.todos;
// Filter by status
if (this.filter.status === "active") {
result = result.filter(t => !t.completed);
} else if (this.filter.status === "completed") {
result = result.filter(t => t.completed);
}
// Filter by search
if (this.filter.searchQuery) {
const q = this.filter.searchQuery.toLowerCase();
result = result.filter(t => t.text.toLowerCase().includes(q));
}
// Filter by tag
if (this.filter.tagFilter) {
result = result.filter(t => t.tags.includes(this.filter.tagFilter!));
}
return result;
}
get activeCount(): number {
return this.todos.filter(t => !t.completed).length;
}
get completedCount(): number {
return this.todos.filter(t => t.completed).length;
}
// Action with typed parameters
addTodo(text: string, tags: string[] = []): void {
this.todos.push({
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date(),
tags
});
}
toggleTodo(id: string): void {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
updateFilter(partial: Partial): void {
this.filter = { ...this.filter, ...partial };
}
// Typed async action using generator
*fetchTodos(): Generator {
this.isLoading = true;
this.error = null;
try {
const response: Response = yield fetch("/api/todos");
const data: Todo[] = yield response.json();
this.todos = data;
} catch (err) {
this.error = err instanceof Error ? err.message : "Unknown error";
} finally {
this.isLoading = false;
}
}
}
// Typed React component using the store
import { observer } from "mobx-react-lite";
interface TodoItemProps {
todo: Todo;
onToggle: (id: string) => void;
}
const TodoItem: React.FC = observer(({ todo, onToggle }) => {
return (
<div className={`todo-item ${todo.completed ? "completed" : ""}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
{todo.tags.map(tag => <span key={tag} className="tag">{tag}</span>)}
</div>
);
});
TypeScript with MobX requires minimal ceremony. The class properties serve as both TypeScript types and MobX annotations. Computed getters have their return types inferred, and action methods benefit from parameter type checking.
Level 7: Testing MobX Stores
MobX stores are plain JavaScript objects, making them straightforward to test. Here's a comprehensive testing approach:
import { autorun, reaction, when } from "mobx";
// Test file for TodoStore
describe("TodoStore", () => {
let store;
beforeEach(() => {
store = new TypedTodoStore();
});
test("addTodo should add a new todo item", () => {
store.addTodo("Buy groceries", ["shopping"]);
expect(store.todos).toHaveLength(1);
expect(store.todos[0].text).toBe("Buy groceries");
expect(store.todos[0].completed).toBe(false);
expect(store.todos[0].tags).toEqual(["shopping"]);
});
test("toggleTodo should flip completion status", () => {
store.addTodo("Test todo");
const id = store.todos[0].id;
store.toggleTodo(id);
expect(store.todos[0].completed).toBe(true);
store.toggleTodo(id);
expect(store.todos[0].completed).toBe(false);
});
test("filteredTodos should filter by status correctly", () => {
store.addTodo("Todo 1");
store.addTodo("Todo 2");
store.addTodo("Todo 3");
// Complete the second todo
store.toggleTodo(store.todos[1].id);
// Test "active" filter
store.updateFilter({ status: "active" });
expect(store.filteredTodos).toHaveLength(2);
// Test "completed" filter
store.updateFilter