.watch
calls are (not) weird
Sometimes, you can notice a weird behavior in your code if you use .watch
to track Store changes. Let us explain what is going on and how to deal with it.
Effector's main mantra
Summary
.watch
method immediately executes callback after module execution with the current value of the Store.
Effector is based on the idea of explicit initialization. It means that module execution should not produce any side effects. It is a good practice because it allows you to control the order of execution and avoid unexpected behavior. This mantra leads us to the idea of explicit start of the app.
However, it is one exception to this rule: callback in .watch
call on Store is executed immediately after the store is created with a current value. This behavior is not quite obvious, but it is introduced on purpose.
Why?
Effector introduced this behavior to be compatible with default behavior of Redux on the early stages of development. Also, it allows using Effector Stores in Svelte as its native stores without any additional compatibility layers.
It is not a case anymore, but we still keep this behavior for historical reasons.
The problem and solutions
Now, let us consider the following example:
const $store = createStore('original value');
$store.watch((value) => {
console.log(value);
});
const scope = fork({
values: [[$store, 'forked value']],
});
// -> 'original value'
In this example, console will print only "original value"
since fork
call does not produce any side effects.
Even if we change order of calls, it will not change the behavior:
const $store = createStore('original value');
const scope = fork({
values: [[$store, 'forked value']],
});
$store.watch((value) => {
console.log(value);
});
// -> 'original value'
It could be confusing, but it is not a bug. First .watch
call executes only with current value of the Store outside of Scope. In real-world applications, it means that you probably should not use .watch
.
Current value?
Actually, yes. Callback executes with the current value of the Store outside of Scope. It means, you can change value of the Store before .watch
call and it will be printed in the console:
const $store = createStore('original value');
$store.setState('something new');
$store.watch((value) => {
console.log(value);
});
// -> 'something new'
However, it is a dangerous way, and you have to avoid it in application code.
In general .watch
could be useful for debugging purposes and as a way to track changes in Store and react somehow. Since, it is not a good idea to use it in the production code, let us consider some alternatives.
Debug
Effector's ecosystem provides a way more powerful tool for debugging: patronum/debug. It correctly works with Fork API and has a lot of other useful features.
First, install it as a dependency:
pnpm install patronum
yarn add patronum
npm install patronum
Then, mark Store with debug
method and register Scope with debug.registerScope
method:
import { createStore, fork } from 'effector';
import { debug } from 'patronum';
const $store = createStore('original value');
debug($store);
const scope = fork({
values: [[$store, 'forked value']],
});
debug.registerScope(scope, { name: 'myAppScope' });
// -> [store] $store [getState] original value
// -> [store] (scope: myAppScope) $store [getState] forked value
import { createStore, fork } from 'effector';
const $store = createStore('original value');
$store.watch((value) => console.log('[store] $store', value));
const scope = fork({
values: [[$store, 'forked value']],
});
// -> [store] $store original value
That is it! Furthermore, you can use debug
method not only to debug value of Store but also for track execution of other units like Event or Effect, for trace chain of calls and so on. For more details, please, check patronum/debug documentation.
TIP
Do not forget to remove debug
calls from the production code. To ensure that, you can use effector/no-patronum-debug
rule for ESLint.
React on changes
If you need to react on changes in Store, you can use .updates
property. It is an Event that emits new values of the Store on each update. With a combination of sample
and Effect it allows you to create side effects on changes in Store in a declarative and robust way.
import {
createEffect,
createStore,
createEvent,
sample,
fork,
allSettled,
} from 'effector';
const someSideEffectFx = createEffect((storeValue) => {
console.log('side effect with ', storeValue);
});
const $store = createStore('original value');
const appInited = createEvent();
sample({
clock: [appInited, $store.updates],
source: $store,
target: someSideEffectFx,
});
const scope = fork({
values: [[$store, 'forked value']],
});
allSettled(appInited, { scope });
// -> side effect with forked value
import { createStore, fork } from 'effector';
const $store = createStore('original value');
$store.watch((value) => console.log('side effect with ', value));
const scope = fork({
values: [[$store, 'forked value']],
});
// -> side effect with original value
TIP
Since, Effector is based on idea of explicit triggers, in this example we use explicit start of the app.
This approach not only solve problems that mentioned above but also increases code readability and maintainability. For example, real-world side effects can sometimes fail, and you need to handle errors. With .watch
approach, you need to handle errors in each callback. With Effect approach, you can handle errors in seamless declarative way, because Effect has a built-in property .fail
which is an Event that emits on each failure.
Summary
- Do not use
.watch
for debug - usepatronum/debug
instead - Do not use
.watch
for logic and side effects - use Effects instead