Best Practices in Effector
This section contains recommendations for effective work with Effector, based on community experience and the development team.
Keep Stores Small
Unlike Redux, in Effector it’s recommended to make stores as atomic as possible. Let’s explore why this is important and what advantages it provides.
Large stores with multiple fields create several problems:
- Unnecessary re-renders: When any field changes, all components subscribed to the store update
- Heavy computations: Each update requires copying the entire object
- Unnecessary calculations: if you have derived stores depending on a large store, they will be recalculated
Atomic stores allow:
- Updating only what actually changed
- Subscribing only to needed data
- More efficient work with reactive dependencies
// ❌ Big store - any change triggers update of everything
const $bigStore = createStore({
profile: {/* many fields */},
settings: {/* many fields */},
posts: [ /* many posts */ ]
})
// ✅ Atomic stores - precise updates
const $userName = createStore('')
const $userEmail = createStore('')
const $posts = createStore<Post[]>([])
const $settings = createStore<Settings>({})
// Component subscribes only to needed data
const UserName = () => {
const name = useUnit($userName) // Updates only when name changes
return <h1>{name}</h1>
}
Rules for atomic stores:
- One store = one responsibility
- Store should be indivisible
- Stores can be combined using combine
- Store update should not affect other data
Immer for Complex Objects
If your store contains nested structures, you can use the beloved Immer for simplified updates:
import { createStore } from "effector";
import { produce } from "immer";
const $users = createStore<User[]>([]);
$users.on(userUpdated, (users, updatedUser) =>
produce(users, (draft) => {
const user = draft.find((u) => u.id === updatedUser.id);
if (user) {
user.profile.settings.theme = updatedUser.profile.settings.theme;
}
}),
);
Explicit Application Start
We recommend using explicit application start through special events to initialize your application.
Why it matters:
- Full control over application lifecycle
- Simplified testing
- Predictable application behavior
- Ability to control initialization order
export const appStarted = createEvent();
call event and subscribe on it:
import { sample } from "effector";
import { scope } from "./app.js";
sample({
clock: appStarted,
target: initFx,
});
appStarted();
import { sample, allSettled } from "effector";
import { scope } from "./app.js";
sample({
clock: appStarted,
target: initFx,
});
allSettled(appStarted, { scope });
Use scope
The effector team recommends always using Scope
, even if your application doesn’t use SSR.
This is necessary so that in the future you can easily migrate to working with Scope
.
useUnit
Hook
Using the useUnit hook is the recommended way to work with units when using frameworks (📘React, 📗Vue, and 📘Solid).
Why you should use useUnit
:
- Correct work with stores
- Optimized updates
- Automatic work with
Scope
– units know which scope they were called in
Pure Functions
Use pure functions everywhere except effects for data processing, this ensures:
- Deterministic result
- No side effects
- Easier to test
- Easier to maintain
If your code can throw an error or can end in success/failure - that’s an excellent place for effects.
Debugging
We strongly recommend using the patronum library and the debug method.
import { createStore, createEvent, createEffect } from "effector";
import { debug } from "patronum/debug";
const event = createEvent();
const effect = createEffect().use((payload) => Promise.resolve("result" + payload));
const $store = createStore(0)
.on(event, (state, value) => state + value)
.on(effect.done, (state) => state * 10);
debug($store, event, effect);
event(5);
effect("demo");
// => [store] $store 1
// => [event] event 5
// => [store] $store 6
// => [effect] effect demo
// => [effect] effect.done {"params":"demo", "result": "resultdemo"}
// => [store] $store 60
However, nothing prevents you from using .watch
or createWatch
for debugging.
Factories
Factory creation is a common pattern when working with effector, it makes it easier to use similar code. However, you may encounter a problem with identical sids that can interfere with SSR.
To avoid this problem, we recommend using the @withease/factories library.
If your environment does not allow adding additional dependencies, you can create your own factory following these guidelines.
Working with Network
For convenient effector work with network requests, you can use farfetched. Farfetched provides:
- Mutations and queries
- Ready API for caching and more
- Framework independence
Effector Utils
The Effector ecosystem includes the patronum library, which provides ready solutions for working with units:
- State management (
condition
,status
, etc.) - Working with time (
debounce
,interval
, etc.) - Predicate functions (
not
,or
,once
, etc.)
Simplifying Complex Logic with createAction
effector-action
is a library that allows you to write imperative code for complex conditional logic while maintaining effector’s declarative nature.
Moreover, effector-action
helps make your code more readable:
import { sample } from "effector";
sample({
clock: formSubmitted,
source: {
form: $form,
settings: $settings,
user: $user,
},
filter: ({ form }) => form.isValid,
fn: ({ form, settings, user }) => ({
data: form,
theme: settings.theme,
}),
target: submitFormFx,
});
sample({
clock: formSubmitted,
source: $form,
filter: (form) => !form.isValid,
target: showErrorMessageFx,
});
sample({
clock: submitFormFx.done,
source: $settings,
filter: (settings) => settings.sendNotifications,
target: sendNotificationFx,
});
import { createAction } from "effector-action";
const submitForm = createAction({
source: {
form: $form,
settings: $settings,
user: $user,
},
target: {
submitFormFx,
showErrorMessageFx,
sendNotificationFx,
},
fn: (target, { form, settings, user }) => {
if (!form.isValid) {
target.showErrorMessageFx(form.errors);
return;
}
target.submitFormFx({
data: form,
theme: settings.theme,
});
},
});
createAction(submitFormFx.done, {
source: $settings,
target: sendNotificationFx,
fn: (sendNotification, settings) => {
if (settings.sendNotifications) {
sendNotification();
}
},
});
submitForm();
Naming
Use accepted naming conventions:
- For stores – prefix
$
- For effects – postfix
fx
, this will help you distinguish your effects from events - For events – no rules, however, we suggest naming events that directly trigger store updates as if they’ve already happened.
const updateUserNameFx = createEffect(() => {});
const userNameUpdated = createEvent();
const $userName = createStore("JS");
$userName.on(userNameUpdated, (_, newName) => newName);
userNameUpdated("TS");
The choice between prefix or postfix is mainly a matter of personal preference. This is necessary to improve the search experience in your IDE.
Anti-patterns
Using watch for Logic
watch should only be used for debugging. For logic, use sample, guard, or effects.
// logic in watch
$user.watch((user) => {
localStorage.setItem("user", JSON.stringify(user));
api.trackUserUpdate(user);
someEvent(user.id);
});
// separate effects for side effects
const saveToStorageFx = createEffect((user: User) =>
localStorage.setItem("user", JSON.stringify(user)),
);
const trackUpdateFx = createEffect((user: User) => api.trackUserUpdate(user));
// connect through sample
sample({
clock: $user,
target: [saveToStorageFx, trackUpdateFx],
});
// for events also use sample
sample({
clock: $user,
fn: (user) => user.id,
target: someEvent,
});
Complex Nested samples
Avoid complex and nested chains of sample.
Abstract Names in Callbacks
Use meaningful names instead of abstract value
, data
, item
.
$users.on(userAdded, (state, payload) => [...state, payload]);
sample({
clock: buttonClicked,
source: $data,
fn: (data) => data,
target: someFx,
});
$users.on(userAdded, (users, newUser) => [...users, newUser]);
sample({
clock: buttonClicked,
source: $userData,
fn: (userData) => userData,
target: updateUserFx,
});
Imperative Calls in Effects
Don’t call events or effects imperatively inside other effects, instead use declarative style.
const loginFx = createEffect(async (params) => {
const user = await api.login(params);
// imperative calls
setUser(user);
redirectFx("/dashboard");
showNotification("Welcome!");
return user;
});
const loginFx = createEffect((params) => api.login(params));
// Connect through sample
sample({
clock: loginFx.doneData,
target: [
$user, // update store
redirectToDashboardFx,
showWelcomeNotificationFx,
],
});
Using getState
Don’t use $store.getState
to get values. If you need to get data from some store, pass it there, for example in source
in sample
:
const submitFormFx = createEffect((formData) => {
// get values through getState
const user = $user.getState();
const settings = $settings.getState();
return api.submit({
...formData,
userId: user.id,
theme: settings.theme,
});
});
// get values through parameters
const submitFormFx = createEffect(({ form, userId, theme }) => {});
// get all necessary data through sample
sample({
clock: formSubmitted,
source: {
form: $form,
user: $user,
settings: $settings,
},
fn: ({ form, user, settings }) => ({
form,
userId: user.id,
theme: settings.theme,
}),
target: submitFormFx,
});
Business Logic in UI
Don’t put your logic in UI elements, this is the main philosophy of effector and what effector tries to free you from, namely the dependency of logic on UI.
Brief summary of anti-patterns:
- Don’t use
watch
for logic, only for debugging - Avoid direct mutations in stores
- Don’t create complex nested
sample
, they’re hard to read - Don’t use large stores, use an atomic approach
- Use meaningful parameter names, not abstract ones
- Don’t call events inside effects imperatively
- Don’t use
$store.getState
for work - Don’t put logic in UI