☄️ Effector operators library delivering modularity and convenience
- 🎲 Try it online: Codesandbox | Playground
- 📦 Source: JSdeliver | Unpkg | NPM | GitHub
- 🦉 Say about it: Twitter
Table of contents
Predicate
- Condition — Trigger then or else by condition.
- Some — Checks that state in at least one store passes the predicate test.
- Every — Checks that state in each store passes the predicate test.
- Reset — Reset all passed stores by clock.
Effect
- Pending — Checks that has effects in pending state.
- InFlight — Counts all pending effects
- Status — Return text representation of effect state.
Timeouts
- Debounce — Creates event which waits until time passes after previous trigger.
- Delay — Delays the call of the event by defined timeout.
- Throttle — Creates event which triggers at most once per timeout.
- Interval — Creates a dynamic interval with any timeout.
- Time — Allows reading current timestamp by triggering clock.
Combination/Decomposition
- CombineEvents — Wait for all passed events is triggered.
- Reshape — Destructure one store to different stores
- SplitMap — Split event to different events and map data.
- Spread — Send fields from object to same targets.
- Snapshot — Create store value snapshot.
- Format — Combine stores to a string literal.
Debug
- Debug — Log triggers of passed units.
💿 Install now
Please, review documentation for YOUR version of patronum not the latest. Find and open tag/release for your version and click on the tag vA.B.C to view repo and documentation for that version, or use "Switch branches/tags" selector.
npm install patronum
Next just import methods from "patronum"
and use it:
import { createEffect } from "effector"
import { status } from "patronum"
const userLoadFx = createEffect()
const $status = status({ effect: userLoadFx })
🐞 Debug and log
Sometimes we need to log each event and change in our application, here we need to install effector-logger
:
npm install --dev effector-logger
We have some variants how to use logger to debug our applications. Please, don't merge all variants, it's not compatible!
1. Temporarily change imports in certain modules
If we need to debug just some list of modules, we can just replace effector
import to effector-logger
:
-import { createStore, createEvent, sample } from 'effector'
+import { createStore, createEvent, sample } from 'effector-logger'
import { spread } from 'patronum'
Next just open the Console in browser DevTools. But here we see strange names of the stores and events like "ashg7d". This means we need to use effector babel plugin.
Note: You don't need to install it separately, because its bundled into effector package.
// .babelrc
{
"plugins": [
["effector/babel-plugin", { "importName": "effector-logger" }], // Just add this line into your .babelrc or babel.config.js plugins section.
],
"presets": [
"patronum/babel-preset" // Add this line at the end of the all presets
]
}
effector-logger/babel-plugin
to automatically replace all imports in development
2. Use But some projects already use effector/babel-plugin
, and for correct work with effector-logger
we need just one instance of babel plugin.
This means that effector-logger has its own babel-plugin.
Don't use effector/babel-plugin
simultaneously with effector-logger/babel-plugin
! Use just one at the time, for example: for the dev environment use effector-logger/babel-plugin
, but for production use effector/babel-plugin
.
How to setup `.babelrc`
// .babelrc
{
"presets": [
"patronum/babel-preset" // Add this line at the end of the all presets in the root of the file
],
"env": {
"development": {
"plugins": [
["effector-logger/babel-plugin", {}] // In the curly brackets you can pass options for logger AND effector
]
},
"production": {
"plugins": [
["effector/babel-plugin", {}] // In the curly brackets you can pass options for effector
]
},
},
}
If you need to pass factories, here you need to duplicate your array:
// .babelrc
{
"env": {
"development": {
"plugins": [
["effector-logger/babel-plugin", {
"effector": { "factories": ["src/shared/lib/compare", "src/shared/lib/timing"] }
}]
]
},
"production": {
"plugins": [
["effector/babel-plugin", { "factories": ["src/shared/lib/compare", "src/shared/lib/timing"] }]
]
},
},
}
Also, you need to build your project with BABEL_ENV=development
for dev and BABEL_ENV=production
for prod, to choose the appropriate option in the "env"
section.
Relative links:
How to setup `babel.config.js`
module.exports = (api) => {
const isDev = api.env("development")
return {
presets: [
// Add next line at the end of presets list
"patronum/babel-preset",
],
plugins: [
// Add next lines at the end of the plugins list
isDev
? ["effector-logger/babel-plugin", {}]
: ["effector/babel-plugin", {}]
]
}
}
If you want to pass factories to the effector plugin, you need just put it to the variable:
module.exports = (api) => {
const isDev = api.env("development")
// Here your factories
const factories = ["src/shared/lib/compare", "src/shared/lib/timing"]
return {
plugins: [
isDev
// All effector options passed into `effector` property
? ["effector-logger/babel-plugin", { effector: { factories } }]
: ["effector/babel-plugin", { factories }]
]
}
}
Also, you need to build your project with BABEL_ENV=development
for dev and BABEL_ENV=production
for prod, to choose the appropriate option in the "env"
section.
Relative links:
macros
3. CRA support withbabel-plugin-macros
is bundled into CRA, so we can use it due CRA don't support adding babel plugins into .babelrc
or babel.config.js
.
Just import from patronum/macro
and effector-logger/macro
, and use as early:
import { createStore, createEffect, sample } from "effector-logger/macro"
import { status, splitMap, combineEvents } from "patronum/macro";
- Warning: babel-plugin-macros do not support
import * as name
!- Note: Since release of patronum@2.0.0 it is required to use babel-plugin-macros@3.0.0 or higher.
- Please note, that react-scripts@4.0.3 and older uses outdated version of this plugin - you can either use yarn resolutions or use react-scripts@5.0.0 or higher.
Migration guide
show / hide
v2.0.0
Removed support of effector v21. Now the minimum supported version is v22.1.2
.
v0.110
From v0.110.0
patronum removed support of effector v20. Now minimum supported version is v21.4
.
Please, before upgrade review release notes of effector v21
.
v0.100
From v0.100.0
patronum introduced object arguments form with BREAKING CHANGES. Please, review migration guide before upgrade from v0.14.x
on your project.
Condition
import { createEvent } from 'effector';
import { condition } from 'patronum/condition';
const trigger = createEvent<string>();
const longString = createEvent<string>();
const shortString = createEvent<string>();
condition({
source: trigger,
if: (string) => string.length > 6,
then: longString,
else: shortString,
});
longString.watch((str) => console.log('long', str));
shortString.watch((str) => console.log('short', str));
trigger('hi'); // => short hi
trigger('welcome'); // => long welcome
Delay
import { createEvent } from 'effector';
import { delay } from 'patronum/delay';
const trigger = createEvent<string>(); // createStore or createEffect
// `timeout` also supports (payload) => number and Store<number>
const delayed = delay({ source: trigger, timeout: 300 });
delayed.watch((payload) => console.info('triggered', payload));
trigger('hello');
// after 300ms
// => triggered hello
Debounce
import { createEvent } from 'effector';
import { debounce } from 'patronum/debounce';
// You should call this event
const trigger = createEvent<number>();
const target = debounce({ source: trigger, timeout: 200 });
target.watch((payload) => console.info('debounced', payload));
trigger(1);
trigger(2);
trigger(3);
trigger(4);
// after 200ms
// => debounced 4
Throttle
import { createEvent } from 'effector';
import { throttle } from 'patronum/throttle';
// You should call this event
const trigger = createEvent<number>();
const target = throttle({ source: trigger, timeout: 200 });
target.watch((payload) => console.info('throttled', payload));
trigger(1);
trigger(2);
trigger(3);
trigger(4);
// 200ms after trigger(1)
// => throttled 4
Interval
import { createStore, createEvent } from 'effector';
import { interval } from 'patronum';
const startCounter = createEvent();
const stopCounter = createEvent();
const $counter = createStore(0);
const { tick } = interval({
timeout: 500,
start: startCounter,
stop: stopCounter,
});
$counter.on(tick, (number) => number + 1);
$counter.watch((value) => console.log('COUNTER', value));
startCounter();
setTimeout(() => stopCounter(), 5000);
Debug
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
Status
import { createEvent, createEffect } from 'effector';
import { status } from 'patronum/status';
const effect = createEffect().use(() => Promise.resolve(null));
const $status = status({ effect });
$status.watch((value) => console.log(`status: ${value}`));
// => status: "initial"
effect();
// => status: "pending"
// => status: "done"
Spread
import { createEvent, createStore } from 'effector';
import { spread } from 'patronum/spread';
const trigger = createEvent<{ first: string; second: string }>();
const $first = createStore('');
const $second = createStore('');
spread({
source: trigger,
targets: {
first: $first,
second: $second,
},
});
trigger({ first: 'Hello', second: 'World' });
$first.watch(console.log); // => Hello
$second.watch(console.log); // => World
Snapshot
import { restore, createEvent } from 'effector';
import { snapshot } from 'patronum/snapshot';
const changeText = createEvent<string>();
const createSnapshot = createEvent();
const $original = restore(changeText, 'Example');
const $snapshot = snapshot({
source: $original,
clock: createSnapshot,
});
changeText('New text');
// $original -> Store with "New text"
// $snapshot -> Store with "Example"
createSnapshot();
// $original -> Store with "New text"
// $snapshot -> Store with "New text"
CombineEvents
Call target event when all event from object/array is triggered
import { createEvent } from 'effector';
import { combineEvents } from 'patronum/combine-events';
const event1 = createEvent();
const event2 = createEvent();
const event3 = createEvent();
const reset = createEvent();
const event = combineEvents({
reset,
events: {
event1,
event2,
event3,
},
});
event.watch((object) => console.log('triggered', object));
event1(true); // nothing
event2('demo'); // nothing
event3(5); // => triggered { event1: true, event2: "demo", event3: 5 }
event1(true); // nothing
event2('demo'); // nothing
reset();
event3(5); // nothing
event1(true); // nothing
event2('demo'); // nothing
event3(5); // => triggered { event1: true, event2: "demo", event3: 5 }
Every
import { createStore } from 'effector';
import { every } from 'patronum/every';
const $isPasswordCorrect = createStore(true);
const $isEmailCorrect = createStore(true);
const $isFormCorrect = every([$isPasswordCorrect, $isEmailCorrect], true);
$isFormCorrect.watch(console.log); // => true
InFlight
import { createEffect } from 'effector';
import { inFlight } from 'patronum/in-flight';
const firstFx = createEffect().use(() => Promise.resolve(1));
const secondFx = createEffect().use(() => Promise.resolve(2));
const $allInFlight = inFlight({ effects: [firstFx, secondFx] });
firstFx();
secondFx();
firstFx();
$allInFlight.watch(console.log);
// => 3
// => 2
// => 1
// => 0
Pending
import { createEffect } from 'effector';
import { pending } from 'patronum/pending';
const loadFirst = createEffect().use(() => Promise.resolve(null));
const loadSecond = createEffect().use(() => Promise.resolve(2));
const $processing = pending({ effects: [loadFirst, loadSecond] });
$processing.watch((processing) => console.info(`processing: ${processing}`));
// => processing: false
loadFirst();
loadSecond();
// => processing: true
// => processing: false
Some
import { createStore, restore, createEvent } from 'effector';
import { some } from 'patronum/some';
const widthSet = createEvent<number>();
const $width = restore(widthSet, 820);
const $height = createStore(620);
const $tooBig = some({
predicate: (size) => size > 800,
stores: [$width, $height],
});
$tooBig.watch((big) => console.log('big', big)); // => big true
widthSet(200);
// => big false
Reshape
import { createStore } from 'effector';
import { reshape } from 'patronum/reshape';
const $original = createStore<string>('Hello world');
const parts = reshape({
source: $original,
shape: {
length: (string) => string.length,
first: (string) => string.split(' ')[0] || '',
second: (string) => string.split(' ')[1] || '',
},
});
parts.length.watch(console.info); // 11
parts.first.watch(console.log); // "Hello"
parts.second.watch(console.log); // "world"
SplitMap
import { createEvent } from 'effector';
import { splitMap } from 'patronum/split-map';
type Action =
| { type: 'update'; content: string }
| { type: 'created'; value: number }
| { type: 'another' };
const serverActionReceived = createEvent<Action>();
const received = splitMap({
source: serverActionReceived,
cases: {
update: (action) => (action.type === 'update' ? action.content : undefined),
created: (action) => (action.type === 'created' ? action.value : undefined),
},
});
received.update.watch((payload) =>
console.info('update received with content:', payload),
);
received.created.watch((payload) => console.info('created with value:', payload));
received.__.watch((payload) => console.info('unknown action received:', payload));
serverActionReceived({ type: 'created', value: 1 });
// => created with value: 1
serverActionReceived({ type: 'update', content: 'demo' });
// => update received with content: "demo"
serverActionReceived({ type: 'another' });
// => unknown action received: { type: "another" }
Time
import { createEvent } from 'effector';
import { time } from 'patronum/time';
const readTime = createEvent();
const $now = time({ clock: readTime });
$now.watch((now) => console.log('Now is:', now));
// => Now is: 1636914286675
readTime();
// => Now is: 1636914300691
Format
import { createStore } from 'effector';
import { format } from 'patronum';
const $firstName = createStore('John');
const $lastName = createStore('Doe');
const $fullName = format`${$firstName} ${$lastName}`;
$fullName.watch(console.log);
// => John Doe
Reset
import { createEvent, createStore } from 'effector';
import { reset } from 'patronum/reset';
const pageUnmounted = createEvent();
const userSessionFinished = createEvent();
const $post = createStore(null);
const $comments = createStore([]);
const $draftComment = createStore('');
reset({
clock: [pageUnmounted, userSessionFinished],
target: [$post, $comments, $draftComment],
});
Development
You can review CONTRIBUTING.md
Release process
- Check out the draft release.
- All PRs should have correct labels and useful titles. You can review available labels here.
- Update labels for PRs and titles, next manually run the release drafter action to regenerate the draft release.
- Review the new version and press "Publish"
- If required check "Create discussion for this release"