Чтобы добавить перевод, откройте Pull Request по этой ссылке.
Отображается содержимое для языка по умолчанию.
Migrating from Redux
If you are coming from Redux, this guide will help you understand how Effector’s concepts map to what you already know and how to translate common Redux patterns into idiomatic Effector code.
For gradual, incremental migration of an existing Redux codebase (where Redux and Effector coexist), see the dedicated @withease/redux migration guide.
This page focuses on the conceptual differences and pattern translations to help you think in Effector.
Concept mapping
| Redux | Effector | Notes |
|---|---|---|
| Store (single, global) | Store (createStore) | Effector uses many small stores instead of one big state tree |
| Action | Event (createEvent) | Events are first-class callable units, no action types or creators needed |
| Reducer | .on() handler | Stores subscribe to events via .on(event, reducer) |
| Thunk / Saga | Effect (createEffect) | A container for side effects with built-in pending/done/fail events |
useSelector | useUnit | From effector-react; subscribes to stores reactively |
useDispatch | useUnit | Events and effects returned by useUnit are already bound |
combineReducers | combine | Derives a new store from multiple stores |
| Middleware | sample / Effect | sample connects units declaratively; effects handle side effects |
Reselect (createSelector) | .map() / combine | Derived stores update only when their source changes |
Redux Toolkit createSlice | Store + Events + .on() | No boilerplate wrapper needed |
Key differences
Many stores instead of one
Redux puts all state into a single store and uses selectors to extract slices. Effector encourages many independent stores, each holding a specific piece of state:
// Redux: one store, nested stateconst initialState = { count: 0, user: null, todos: [] };
// Effector: independent storesconst $count = createStore(0);const $user = createStore(null);const $todos = createStore([]);This eliminates the need for deeply nested state updates and makes each store independently testable.
No action types or switch statements
Redux actions require string types and reducers use switch statements. In Effector, events are callable functions and stores subscribe to them directly:
// Reduxconst INCREMENT = "INCREMENT";const increment = () => ({ type: INCREMENT });
function counterReducer(state = 0, action) { switch (action.type) { case INCREMENT: return state + 1; default: return state; }}
// Effectorconst increment = createEvent();const $count = createStore(0).on(increment, (state) => state + 1);
// Usage: just call the eventincrement();Declarative connections with sample
Where Redux uses middleware (thunks, sagas) for orchestration, Effector uses sample to declaratively connect units:
// Redux: thunk with imperative dispatchconst fetchUser = (id) => async (dispatch) => { dispatch(setLoading(true)); const user = await api.getUser(id); dispatch(setUser(user)); dispatch(setLoading(false));};
// Effector: declarative connectionsconst pageOpened = createEvent<number>(); // carries user idconst fetchUserFx = createEffect((id: number) => api.getUser(id));const $user = createStore(null).on(fetchUserFx.doneData, (_, user) => user);const $loading = fetchUserFx.pending; // built-in!
sample({ clock: pageOpened, // id comes from the event payload target: fetchUserFx,});Notice that $loading comes for free — every effect has a .pending store and .done / .fail / .doneData / .failData events built in.
Common patterns
Counter
// Redux (with Redux Toolkit)const counterSlice = createSlice({ name: "counter", initialState: 0, reducers: { incremented: (state) => state + 1, decremented: (state) => state - 1, incrementedBy: (state, action) => state + action.payload, },});
// Effectorconst incremented = createEvent();const decremented = createEvent();const incrementedBy = createEvent();
const $counter = createStore(0) .on(incremented, (n) => n + 1) .on(decremented, (n) => n - 1) .on(incrementedBy, (n, amount) => n + amount);Async data fetching
// Redux: createAsyncThunkconst fetchTodos = createAsyncThunk("todos/fetch", async () => { const response = await fetch("/api/todos"); return response.json();});
const todosSlice = createSlice({ name: "todos", initialState: { items: [], status: "idle", error: null }, extraReducers: (builder) => { builder .addCase(fetchTodos.pending, (state) => { state.status = "loading"; }) .addCase(fetchTodos.fulfilled, (state, action) => { state.status = "succeeded"; state.items = action.payload; }) .addCase(fetchTodos.rejected, (state, action) => { state.status = "failed"; state.error = action.error.message; }); },});
// Effectorconst fetchTodosFx = createEffect(async () => { const response = await fetch("/api/todos"); return response.json();});
const $todos = createStore([]).on(fetchTodosFx.doneData, (_, todos) => todos);const $loading = fetchTodosFx.pending;const $error = createStore(null) .on(fetchTodosFx.failData, (_, error) => error.message) .reset(fetchTodosFx);Effector’s createEffect gives you .pending, .done, .fail, .doneData, and .failData for free — no need to manually track loading states.
Derived state
// Redux: Reselectconst selectCompletedTodos = createSelector( (state) => state.todos, (todos) => todos.filter((t) => t.completed));
const selectCompletedCount = createSelector( selectCompletedTodos, (todos) => todos.length);
// Effector: .map() and combine()const $completedTodos = $todos.map((todos) => todos.filter((t) => t.completed));const $completedCount = $completedTodos.map((todos) => todos.length);
// Or combine multiple storesconst $filter = createStore("all"); // e.g., "all" | "active" | "completed"const $filteredTodos = combine($todos, $filter, (todos, filter) => filter === "all" ? todos : todos.filter((t) => t.status === filter));Conditional logic (replacing middleware)
// Redux: middleware or thunk with conditionalsconst conditionalFetch = (id) => (dispatch, getState) => { if (getState().cache[id]) return; dispatch(fetchItem(id));};
// Effector: sample with filterconst itemRequested = createEvent<string>(); // carries item idconst fetchItemFx = createEffect((id: string) => api.getItem(id));const $cache = createStore<Record<string, Item>>({});
sample({ clock: itemRequested, source: $cache, filter: (cache, id) => !cache[id], // skip if already cached fn: (_, id) => id, target: fetchItemFx,});React integration
// Reduximport { useSelector, useDispatch } from "react-redux";
function Counter() { const count = useSelector((state) => state.counter); const dispatch = useDispatch(); return <button onClick={() => dispatch(increment())}>Count: {count}</button>;}
// Effectorimport { useUnit } from "effector-react";
function Counter() { const { count, increment: onIncrement } = useUnit({ count: $counter, increment: incremented, }); return <button onClick={onIncrement}>Count: {count}</button>;}With useUnit, stores are subscribed reactively and events are already bound — no dispatch wrapper needed.
Testing
Effector provides fork and allSettled for isolated testing without mocking global state:
// Redux: requires configuring a mock storeconst mockStore = configureStore({ reducer: rootReducer });mockStore.dispatch(increment());expect(mockStore.getState().counter).toBe(1);
// Effector: fork creates an isolated scopeimport { fork, allSettled } from "effector";
const scope = fork();await allSettled(incremented, { scope });expect(scope.getState($counter)).toBe(1);Each test gets a completely independent scope with no shared mutable state between tests.
Migration strategy
- Start small: Pick one isolated feature and rewrite it in Effector
- Coexist: Use
@withease/reduxto bridge Redux and Effector during the transition (see the gradual migration guide) - Move outward: Migrate features one by one, replacing selectors with stores and thunks with effects
- Remove Redux: Once all features are migrated, remove the Redux dependency
Further reading
- Core Concepts — Effector fundamentals in depth
- API Reference — full API documentation
- @withease/redux — gradual migration tooling
- Best Practices — naming conventions, patterns, and tips
- Testing — testing with
forkandallSettled
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.