# Effector Effector is a powerful, lightweight state management library for JavaScript applications that works seamlessly with React, Vue, Svelte, Solid, and vanilla JavaScript. It implements business logic through a reactive data flow model built on three core primitives: **stores** (state containers), **events** (triggers for state changes), and **effects** (async operations). Effector provides excellent TypeScript support out of the box and follows principles of atomic stores, predictable state updates, and framework-agnostic design. The library excels at separating business logic from UI components, enabling complex data flows through declarative unit composition. With built-in support for server-side rendering (SSR) via scoped state isolation (`fork`), testing through dependency injection, and debugging tools, Effector provides a complete solution for managing application state at any scale while maintaining code that is easy to reason about and test. ## Installation ```bash # Core library npm install effector # React bindings npm install effector effector-react # Vue bindings npm install effector effector-vue # Solid bindings npm install effector effector-solid ``` --- ## createEvent Creates an event that acts as an entry point into the reactive data flow. Events represent user actions, commands, or intentions to change state and can be called directly to trigger state updates throughout your application. ```ts import { createEvent, createStore } from "effector"; // Create events for user actions const increment = createEvent(); const decrement = createEvent(); const addAmount = createEvent(); const reset = createEvent(); // Create a store that reacts to events const $counter = createStore(0) .on(increment, (state) => state + 1) .on(decrement, (state) => state - 1) .on(addAmount, (state, amount) => state + amount) .reset(reset); // Watch store changes $counter.watch((count) => console.log("Count:", count)); // => Count: 0 // Trigger events increment(); // => Count: 1 increment(); // => Count: 2 addAmount(10); // => Count: 12 decrement(); // => Count: 11 reset(); // => Count: 0 // Derived events with .map() const userUpdated = createEvent<{ name: string; role: string }>(); const nameChanged = userUpdated.map(({ name }) => name); const roleChanged = userUpdated.map(({ role }) => role.toUpperCase()); nameChanged.watch((name) => console.log("Name:", name)); roleChanged.watch((role) => console.log("Role:", role)); userUpdated({ name: "John", role: "admin" }); // => Name: John // => Role: ADMIN ``` --- ## createStore Creates a store that holds state values and updates when triggered by events or effects. Stores are immutable - they only update when the new value is strictly different (`!==`) from the current one, and changes must return new object references. ```ts import { createEvent, createStore } from "effector"; // Basic store with event handlers const addTodo = createEvent<{ id: number; text: string; completed: boolean }>(); const toggleTodo = createEvent(); const clearCompleted = createEvent(); const $todos = createStore>([]) .on(addTodo, (todos, newTodo) => [...todos, newTodo]) .on(toggleTodo, (todos, id) => todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ) .on(clearCompleted, (todos) => todos.filter(todo => !todo.completed)); // Derived stores using .map() const $completedCount = $todos.map(todos => todos.filter(t => t.completed).length); const $pendingCount = $todos.map(todos => todos.filter(t => !t.completed).length); // Watch all stores $todos.watch(todos => console.log("Todos:", todos)); $completedCount.watch(count => console.log("Completed:", count)); $pendingCount.watch(count => console.log("Pending:", count)); // Add todos addTodo({ id: 1, text: "Learn Effector", completed: false }); // => Todos: [{ id: 1, text: "Learn Effector", completed: false }] // => Completed: 0 // => Pending: 1 addTodo({ id: 2, text: "Build app", completed: false }); toggleTodo(1); // => Completed: 1 // => Pending: 1 // Store with configuration options const $user = createStore<{ name: string } | null>(null, { name: "user", // Debug name skipVoid: true, // Skip undefined values (default) serialize: "ignore", // Exclude from SSR serialization }); ``` --- ## createEffect Creates an effect for handling asynchronous operations like API calls, timers, or any side effects. Effects provide built-in events for tracking pending state, success (`.done`/`.doneData`), and failure (`.fail`/`.failData`). ```ts import { createEffect, createStore, createEvent } from "effector"; // Define the effect with async handler interface User { id: number; name: string; email: string; } const fetchUserFx = createEffect(async (userId) => { const response = await fetch(`https://api.example.com/users/${userId}`); if (!response.ok) { throw new Error(`Failed to fetch user: ${response.status}`); } return response.json(); }); // Create stores that react to effect lifecycle const $user = createStore(null) .on(fetchUserFx.doneData, (_, user) => user) .reset(fetchUserFx.fail); const $isLoading = fetchUserFx.pending; // Built-in pending store const $error = createStore(null) .on(fetchUserFx.failData, (_, error) => error.message) .reset(fetchUserFx); // Watch effect lifecycle events fetchUserFx.watch((params) => console.log("Fetching user:", params)); fetchUserFx.done.watch(({ params, result }) => { console.log(`User ${params} fetched:`, result.name); }); fetchUserFx.fail.watch(({ params, error }) => { console.error(`Failed to fetch user ${params}:`, error.message); }); fetchUserFx.finally.watch(({ params, status }) => { console.log(`Request for user ${params} ${status}`); }); // Trigger the effect await fetchUserFx(123); // => Fetching user: 123 // => User 123 fetched: John Doe // => Request for user 123 done // Multiple effects with shared error handling const saveUserFx = createEffect(async (user: Partial) => { const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(user), }); return response.json(); }); const deleteUserFx = createEffect(async (userId) => { await fetch(`/api/users/${userId}`, { method: "DELETE" }); }); ``` --- ## sample Connects units (stores, events, effects) together by taking data from a `source` and sending it to a `target` when a `clock` triggers. This is the primary operator for building reactive data flows and unit composition in Effector. ```ts import { createEvent, createStore, createEffect, sample } from "effector"; // Basic sample: trigger effect with store data when button clicked const submitClicked = createEvent(); const $formData = createStore({ email: "", password: "" }); const submitFormFx = createEffect<{ email: string; password: string }, void>(); sample({ clock: submitClicked, // When this triggers source: $formData, // Read from this store target: submitFormFx, // Send data to this effect }); // Sample with filter: only proceed if condition is met const $isValid = createStore(false); sample({ clock: submitClicked, source: $formData, filter: $isValid, // Only proceed if store value is true target: submitFormFx, }); // Sample with fn: transform data before passing const userSelected = createEvent<{ id: number; name: string }>(); const $currentUserId = createStore(null); sample({ clock: userSelected, fn: (user) => user.id, // Extract just the id target: $currentUserId, }); // Sample with source and fn: combine store data with event payload const quantityChanged = createEvent(); const $price = createStore(100); const $total = createStore(0); sample({ clock: quantityChanged, source: $price, fn: (price, quantity) => price * quantity, target: $total, }); // Sample with filter function const scoreUpdated = createEvent(); const showCelebrationFx = createEffect(); sample({ clock: scoreUpdated, filter: (score) => score >= 100, // Only celebrate high scores target: showCelebrationFx, }); // Sample without target returns a new unit const $firstName = createStore("John"); const $lastName = createStore("Doe"); const updateProfile = createEvent(); const $fullName = sample({ clock: updateProfile, source: { first: $firstName, last: $lastName }, fn: ({ first, last }) => `${first} ${last}`, }); // $fullName is a derived store that updates when updateProfile fires ``` --- ## combine Creates a derived store by combining multiple stores into one. The resulting store automatically updates whenever any of the source stores change, and supports transformation functions for custom logic. ```ts import { createStore, createEvent, combine } from "effector"; // Combine stores into an object const $firstName = createStore("John"); const $lastName = createStore("Doe"); const $age = createStore(30); const $user = combine({ firstName: $firstName, lastName: $lastName, age: $age, }); $user.watch(console.log); // => { firstName: "John", lastName: "Doe", age: 30 } // Combine with transformation function const $fullName = combine( { firstName: $firstName, lastName: $lastName }, ({ firstName, lastName }) => `${firstName} ${lastName}` ); // => "John Doe" // Combine stores as array const $coordinates = combine([$x, $y, $z]); // => [10, 20, 30] // Combine multiple stores with transformation const $price = createStore(100); const $quantity = createStore(2); const $discount = createStore(0.1); const $total = combine( $price, $quantity, $discount, (price, qty, disc) => { const subtotal = price * qty; return subtotal - (subtotal * disc); } ); // => 180 // Form validation example const $email = createStore(""); const $password = createStore(""); const $confirmPassword = createStore(""); const $validation = combine( { email: $email, password: $password, confirmPassword: $confirmPassword }, ({ email, password, confirmPassword }) => { const errors: string[] = []; if (!email.includes("@")) errors.push("Invalid email"); if (password.length < 8) errors.push("Password too short"); if (password !== confirmPassword) errors.push("Passwords don't match"); return { isValid: errors.length === 0, errors }; } ); // Mix stores with static values const $config = combine({ userId: $userId, apiUrl: "https://api.example.com", // Static value version: 2, // Static value }); ``` --- ## attach Creates new effects based on existing ones with the ability to automatically inject data from stores. This enables reusing effect logic with different parameters and creating module-specific effect instances. ```ts import { createStore, createEffect, attach } from "effector"; // Base API effect const apiFx = createEffect<{ url: string; token: string; data?: any }, any>( async ({ url, token, data }) => { const response = await fetch(url, { method: data ? "POST" : "GET", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, body: data ? JSON.stringify(data) : undefined, }); return response.json(); } ); // Store with auth token const $authToken = createStore("my-secret-token"); // Create attached effect that auto-injects token const fetchUserFx = attach({ effect: apiFx, source: $authToken, mapParams: (userId: number, token) => ({ url: `/api/users/${userId}`, token, }), }); // Now you can call without providing token await fetchUserFx(123); // Attach with source object const $credentials = createStore({ token: "", baseUrl: "https://api.example.com" }); const createPostFx = attach({ effect: apiFx, source: $credentials, mapParams: (post: { title: string; body: string }, { token, baseUrl }) => ({ url: `${baseUrl}/posts`, token, data: post, }), }); // Create module-local effect copies for isolated event handling const sendAnalyticsFx = createEffect<{ event: string; data: any }, void>(); // Auth module analytics const trackAuthFx = attach({ effect: sendAnalyticsFx }); trackAuthFx.done.watch(() => console.log("Auth event tracked")); // Cart module analytics const trackCartFx = attach({ effect: sendAnalyticsFx }); trackCartFx.done.watch(() => console.log("Cart event tracked")); // Each module can watch only its own effect events trackAuthFx({ event: "login", data: {} }); // => Auth event tracked (cart handler not called) ``` --- ## fork Creates an isolated scope for running application logic without affecting global state. Essential for server-side rendering (SSR), testing, and running multiple independent instances of your application. ```ts import { createStore, createEvent, createEffect, fork, allSettled, serialize } from "effector"; // Define your application units const increment = createEvent(); const fetchDataFx = createEffect(async () => { return ["item1", "item2", "item3"]; }); const $counter = createStore(0).on(increment, (n) => n + 1); const $data = createStore([]).on(fetchDataFx.doneData, (_, data) => data); // Create isolated scopes const scopeA = fork(); const scopeB = fork(); // Run events in specific scopes await allSettled(increment, { scope: scopeA }); await allSettled(increment, { scope: scopeA }); await allSettled(increment, { scope: scopeB }); // Each scope has independent state console.log(scopeA.getState($counter)); // => 2 console.log(scopeB.getState($counter)); // => 1 console.log($counter.getState()); // => 0 (global unchanged) // Fork with initial values (for SSR hydration) const clientScope = fork({ values: [ [$counter, 10], [$data, ["cached1", "cached2"]], ], }); console.log(clientScope.getState($counter)); // => 10 console.log(clientScope.getState($data)); // => ["cached1", "cached2"] // Fork with mocked handlers (for testing) const testScope = fork({ handlers: [ [fetchDataFx, async () => ["mock1", "mock2"]], // Mock the effect ], }); await allSettled(fetchDataFx, { scope: testScope }); console.log(testScope.getState($data)); // => ["mock1", "mock2"] // Serialize scope for SSR const serverScope = fork(); await allSettled(fetchDataFx, { scope: serverScope }); const serialized = serialize(serverScope); // Send serialized state to client for hydration ``` --- ## split Routes events into different cases based on conditions. Works like pattern matching for event payloads, directing data to different handlers based on matcher functions or store values. ```ts import { createEvent, createEffect, createStore, split } from "effector"; // Split based on matcher functions const messageReceived = createEvent<{ type: string; payload: any }>(); const showNotification = createEvent(); const playSound = createEffect(); const logUnknownFx = createEffect<{ type: string }, void>(); split({ source: messageReceived, match: { text: (msg) => msg.type === "text", audio: (msg) => msg.type === "audio", image: (msg) => msg.type === "image", }, cases: { text: showNotification.prepend((msg) => msg.payload), audio: playSound.prepend((msg) => msg.payload.url), image: showNotification.prepend((msg) => "New image received"), __: logUnknownFx, // Default case for unmatched }, }); messageReceived({ type: "text", payload: "Hello!" }); // => showNotification triggered with "Hello!" messageReceived({ type: "video", payload: {} }); // => logUnknownFx triggered (default case) // Split with case store (dynamic routing) const $currentRoute = createStore<"home" | "profile" | "settings">("home"); const navigate = createEvent(); const showHome = createEvent(); const showProfile = createEvent(); const showSettings = createEvent(); split({ source: navigate, match: $currentRoute, cases: { home: showHome, profile: showProfile, settings: showSettings, }, }); // Split returning events (short form) const message = createEvent(); const { short, medium, long } = split(message, { short: (m) => m.length <= 5, medium: (m) => m.length <= 15, long: (m) => m.length > 15, }); short.watch((m) => console.log("Short:", m)); medium.watch((m) => console.log("Medium:", m)); long.watch((m) => console.log("Long:", m)); message("Hi"); // => Short: Hi message("Hello world"); // => Medium: Hello world message("This is a very long message"); // => Long: ... ``` --- ## merge Combines multiple units (events, effects, or stores) into a single event that triggers when any of the source units fire. Useful for handling multiple events with the same handler. ```ts import { createEvent, createStore, createEffect, merge } from "effector"; // Merge multiple events const buttonClicked = createEvent(); const formSubmitted = createEvent(); const keyPressed = createEvent(); const anyInteraction = merge([buttonClicked, formSubmitted, keyPressed]); anyInteraction.watch((payload) => { console.log("User interaction detected:", payload); }); buttonClicked(); // => User interaction detected: undefined formSubmitted(); // => User interaction detected: undefined keyPressed("Enter"); // => User interaction detected: Enter // Merge store updates const $firstName = createStore("John"); const $lastName = createStore("Doe"); const nameChanged = merge([$firstName, $lastName]); nameChanged.watch((value) => { console.log("Name changed to:", value); }); // Merge effects for unified error handling const fetchUserFx = createEffect(); const fetchPostsFx = createEffect(); const fetchCommentsFx = createEffect(); const anyRequestFailed = merge([ fetchUserFx.fail, fetchPostsFx.fail, fetchCommentsFx.fail, ]); anyRequestFailed.watch(({ error }) => { console.error("Request failed:", error.message); // Show global error notification }); // Merge for loading state const anyRequestStarted = merge([fetchUserFx, fetchPostsFx, fetchCommentsFx]); const anyRequestFinished = merge([ fetchUserFx.finally, fetchPostsFx.finally, fetchCommentsFx.finally, ]); ``` --- ## useUnit (React) React hook that binds stores, events, and effects to the current scope and returns their values or bound functions. This is the primary way to use Effector in React components. ```tsx import { createStore, createEvent, createEffect, fork } from "effector"; import { useUnit, Provider } from "effector-react"; // Define your stores and events const $count = createStore(0); const $user = createStore<{ name: string } | null>(null); const increment = createEvent(); const decrement = createEvent(); const fetchUserFx = createEffect(); $count.on(increment, (n) => n + 1).on(decrement, (n) => n - 1); $user.on(fetchUserFx.doneData, (_, user) => user); // Use in components function Counter() { // Single store const count = useUnit($count); // Single event (returns bound function) const onIncrement = useUnit(increment); // Multiple units as array const [onInc, onDec] = useUnit([increment, decrement]); // Multiple units as object const { count: c, increment: inc, decrement: dec } = useUnit({ count: $count, increment, decrement, }); return (

Count: {count}

); } function UserProfile() { const [user, isLoading, fetchUser] = useUnit([ $user, fetchUserFx.pending, fetchUserFx, ]); return (
{isLoading &&

Loading...

} {user &&

Welcome, {user.name}!

}
); } // Wrap app with Provider for SSR/scope support function App() { const scope = fork(); return ( ); } ``` --- ## Store Methods Store instances provide methods for reacting to events, transforming data, and managing state. These chainable methods enable declarative state management without imperative code. ```ts import { createEvent, createStore, merge } from "effector"; const $store = createStore(0); // .on(trigger, reducer) - Update state when trigger fires const add = createEvent(); const multiply = createEvent(); $store .on(add, (state, value) => state + value) .on(multiply, (state, value) => state * value); // .on with array of triggers const reset1 = createEvent(); const reset2 = createEvent(); $store.on([reset1, reset2], () => 0); // .reset(...triggers) - Reset to initial state const clearAll = createEvent(); $store.reset(clearAll); // .map(fn) - Create derived store const $doubled = $store.map((value) => value * 2); const $isPositive = $store.map((value) => value > 0); // .watch(watcher) - Subscribe to updates const unwatch = $store.watch((value) => { console.log("Store updated:", value); }); // Later: unwatch() to unsubscribe // .off(trigger) - Remove subscription to trigger $store.off(add); // No longer reacts to add event // Properties console.log($store.defaultState); // Initial value console.log($store.shortName); // Debug name console.log($store.getState()); // Current value (use sparingly) // .updates - Event that fires on every update $store.updates.watch((newValue) => { console.log("Updated to:", newValue); }); // .reinit - Event to reset to default state $store.reinit(); // Same as reset but callable directly ``` --- ## Event Methods Events provide methods for transforming payloads, filtering triggers, and creating derived events. These enable complex event processing pipelines. ```ts import { createEvent } from "effector"; const userAction = createEvent<{ type: string; data: any }>(); // .map(fn) - Transform payload, creates derived event const actionType = userAction.map((action) => action.type); const actionData = userAction.map((action) => action.data); actionType.watch((type) => console.log("Action type:", type)); // .filter({ fn }) - Filter events, creates derived event const importantActions = userAction.filter({ fn: (action) => action.type.startsWith("important_"), }); // .filterMap(fn) - Filter and transform, skip if undefined const numbers = createEvent(); const validNumbers = numbers.filterMap((str) => { const num = parseInt(str, 10); return isNaN(num) ? undefined : num; // Skip invalid }); validNumbers.watch((n) => console.log("Valid number:", n)); numbers("42"); // => Valid number: 42 numbers("abc"); // => (skipped) // .prepend(fn) - Transform before original event const saveUser = createEvent<{ name: string; email: string }>(); const saveUserFromForm = saveUser.prepend((formData: FormData) => ({ name: formData.get("name") as string, email: formData.get("email") as string, })); // saveUserFromForm(formData) -> transforms -> saveUser({ name, email }) // .watch(fn) - Subscribe to event calls const unwatch = userAction.watch((payload) => { console.log("Event triggered:", payload); }); // Chaining example const rawInput = createEvent(); const processedInput = rawInput .map((s) => s.trim()) .filter({ fn: (s) => s.length > 0 }) .map((s) => s.toLowerCase()); processedInput.watch((s) => console.log("Processed:", s)); rawInput(" HELLO "); // => Processed: hello rawInput(" "); // => (filtered out) ``` --- ## Summary Effector provides a complete reactive state management solution centered around three core primitives: **stores** for holding state, **events** for triggering state changes, and **effects** for handling side effects. The `sample` operator serves as the primary mechanism for connecting these units, enabling complex data flows through declarative composition. The `combine` function allows deriving new stores from existing ones, while `split` and `merge` provide event routing and aggregation capabilities. For framework integration, Effector offers dedicated packages like `effector-react` with hooks such as `useUnit` that automatically subscribe components to store updates and bind events to the current scope. The `fork` and `allSettled` APIs enable server-side rendering and testing by creating isolated state scopes with custom initial values and mocked effect handlers. This architecture promotes separation of concerns, testability, and maintainable code by keeping business logic independent of UI frameworks while maintaining full TypeScript support and excellent developer experience.