Event
[ ru ]The Event in effector represents a user action, a step in the application process, a command to execute, or an intention to make modifications, among other things. This unit is designed to be a carrier of information/intention/state within the application, not the holder of a state.
There are two type of events provided by effector: Event
and ReadonlyEvent
.
Construction
There are many ways to create event:
- the most common
createEvent
- using Domain createEvent
- via Event’s methods and it’s supertype ReadonlyEvent’s methods
- some Effect’s methods return new events and readonly events
- operators such as:
createApi
Declaring types
Event carries some data and in TypeScript ecosystem each data should have defined type. When event is explicitly created by createEvent
type of the argument must be provided as a Generic type argument:
import { ItemAdded } from "effector";
interface ItemAdded {
id: string;
title: string;
}
const itemAdded = createEvent<ItemAdded>();
In the most cases there is no reason to use void
with the another type (). Use Event<void | number>
void
only to declare the Event or ReadonlyEvent without the argument at all. That’s why it is possible to send data from event with argument into event without argument.
sample({
clock: withData, // Event<number>
target: withoutData, // Event<void>
});
We’re strongly recommends to use null
for empty values when intended:
import { createEvent } from "effector";
const maybeDataReceived = createEvent<Data | null>();
// maybeDataReceived: Event<Data | null>
Read more in the explanation section.
Methods
All the methods and properties from ReadonlyEvent is also available on Event
instance.
You can think of the Event and ReadonlyEvent as type and its super type:
Event<T> extends ReadonlyEvent<T>
event(argument)
Initiates an event with the provided argument, which in turn activates any registered subscribers.
Read more in the explanation section.
Formulae
const event: Event<T>
event(argument: T): T
event
called as a function always returns its argument as is- all subscribers of event receives the
argument
passed into - when
T
isvoid
,event
can be called without arguments T
by default isvoid
, so generic type argument can be omitted
In Effector, any event supports only a single argument. It is not possible to call an event with two or more arguments, as in someEvent(first, second)
.
All arguments beyond the first will be ignored. The core team has implemented this rule for specific reasons related to the design and functionality.
Arguments
event
is an instance ofEvent<T>
argument
is a value ofT
. It is optional, if event is defined asEvent<void>
.
Returns
T
: Represents the same value that is passed into the event
.
Types
import { createEvent, Event } from "effector";
const someHappened = createEvent<number>();
// someHappened: Event<number>
someHappened(1);
const anotherHappened = createEvent();
// anotherHappened: Event<void>
anotherHappened();
An event can be specified with a single generic type argument. By default, this argument is set to void, indicating that the event does not accept any parameters.
prepend(fn)
Creates a new NOT derived event, that should be called, upon trigger it sends transformed data into the original event.
Works kind of like reverse .map
. In case of .prepend
data transforms before the original event occurs and in the case of .map
, data transforms after original event occurred.
If the original event belongs to some domain, then a new event will belong to it as well.
Formulae
const second = first.prepend(fn);
- When
second
event is triggered - Call
fn
with argument from thesecond
event - Trigger
first
event with the result offn()
Arguments
fn
(Function): A function that receivesargument
, should be pure.
Returns
Event: New event.
Types
There TypeScript requires explicitly setting type of the argument of fn
function:
import { createEvent } from "effector";
const original = createEvent<{ input: string }>();
const prepended = original.prepend((input: string) => ({ input }));
// ^^^^^^ here
Type of the original
event argument and the resulting type of the fn
must be the same.
Example
import { createEvent } from "effector";
const userPropertyChanged = createEvent();
userPropertyChanged.watch(({ field, value }) => {
console.log(`User property "${field}" changed to ${value}`);
});
const changeName = userPropertyChanged.prepend((name) => ({
field: "name",
value: name,
}));
const changeRole = userPropertyChanged.prepend((role) => ({
field: "role",
value: role.toUpperCase(),
}));
changeName("john");
// => User property "name" changed to john
changeRole("admin");
// => User property "role" changed to ADMIN
changeName("alice");
// => User property "name" changed to alice
Meaningful example
You can think of this method like a wrapper function. Let’s assume we have function with not ideal API, but we want to call it frequently:
import { sendAnalytics } from "./analytics";
export function reportClick(item: string) {
const argument = { type: "click", container: { items: [arg] } };
return sendAnalytics(argument);
}
This is exactly how .prepend()
works:
import { sendAnalytics } from "./analytics";
export const reportClick = sendAnalytics.prepend((item: string) => {
return { type: "click", container: { items: [arg] } };
});
reportClick("example");
// reportClick triggered "example"
// sendAnalytics triggered { type: "click", container: { items: ["example"] } }
Check all other methods on ReadonlyEvent.
ReadonlyEvent instance
A ReadonlyEvent is a super type of Event
with different approach. Firstly, invoking a ReadonlyEvent is not allowed, and it cannot be used as a target
in the sample
operator, and so on.
The primary purpose of a ReadonlyEvent is to be triggered by internal code withing the effector library or ecosystem. For instance, the .map()
method returns a ReadonlyEvent, which is subsequently called by the .map()
method itself.
There is no need for user code to directly invoke such a ReadonlyEvent.
If you find yourself needing to call a ReadonlyEvent, it may be necessary to reevaluate and restructure your application’s logic.
All the functionalities provided by ReadonlyEvent are also supported in a regular Event.
Construction
There is no way to manually create ReadonlyEvent, but some methods and operators returns derived events, they are have ReadonlyEvent<T>
type:
- Event’s methods like:
.map(fn)
,.filter({fn})
, and so on - Store’s property: ‘.updates’
- Effect’s methods and properties
- operators like:
sample
,merge
Declaring types
It becomes necessary in cases where a factory or library requires an event to subscribe to its updates, ensuring proper integration and interaction with the provided functionality:
ReadonlyEvent<T>;
Where T
represents the type of the event’s argument.
Read more in the explanation section.
Methods
map(fn)
Creates a new derived ReadonlyEvent, which will be called after the original event is called, using the result of the fn function as its argument. This special function enables you to break down and manage data flow, as well as extract or transform data within your business logic model.
Formulae
const second = first.map(fn);
- When
first
is triggered, pass payload fromfirst
tofn
. - Trigger
second
with the result of thefn()
call as payload. - The function
fn
is invoked each time thefirst
event is triggered. - Also, the
second
event triggered each time thefirst
is triggered.
Arguments
fn
(Function): A function that receivesargument
, should be pure.
Types
The resulting type of the fn
function will be utilized to define the type of the derived event.
import { createEvent } from "effector";
const first = createEvent<number>();
// first: Event<number>
const second = first.map((count) => count.toString());
// second: ReadonlyEvent<string>
The first
event can be represented as either Event<T>
or ReadonlyEvent<T>
.
The second
event will always be represented as ReadonlyEvent<T>
.
Returns
ReadonlyEvent: The new event.
Example
import { createEvent } from "effector";
const userUpdated = createEvent();
// you may decompose dataflow with .map() method
const userNameUpdated = userUpdated.map(({ user }) => name);
// either way you can transform data
const userRoleUpdated = userUpdated.map((user) => user.role.toUpperCase());
userNameUpdated.watch((name) => console.log(`User's name is [${name}] now`));
userRoleUpdated.watch((role) => console.log(`User's role is [${role}] now`));
userUpdated({ name: "john", role: "admin" });
// => User's name is [john] now
// => User's role is [ADMIN] now
filter({ fn })
This method generates a new derived ReadonlyEvent that will be invoked after the original event, but only if the fn
function returns a value of true
. This special function enables you to break down data flow into a branches and subscribe on them within the business logic model.
sample operator with filter
argument is the preferred filtering method.
Formulae
const second = first.filter({ fn });
- When
first
is triggered, pass payload fromfirst
tofn
. - The
second
event will be triggered only iffn
returnstrue
, with the argument fromfirst
event. - The function
fn
is invoked each time thefirst
event is triggered. - Also, the
second
event triggered each time thefirst
is triggered, and thefn
returnedtrue
.
Arguments
fn
(Function): A function that receivesargument
, should be pure.
Here, due to legacy restrictions fn
is required to use object form because event.filter(fn)
was an alias for Event filterMap.
Use it always like this .filter({ fn })
.
Returns
ReadonlyEvent: The new event
Types
Method .filter()
always returns ReadonlyEvent. Also this event will have the same type as the original type:
import { createEvent } from "effector";
const numberReceived = createEvent<number>();
// numberReceived: Event<number>
const evenReceived = numberReceived.filter({
fn: (number) => number % 2 === 0,
});
// evenReceived: ReadonlyEvent<number>
evenReceived.watch(console.info);
numberReceived(5); // nothing
numberReceived(2); // => 2
Example
import { createEvent, createStore } from "effector";
const numbers = createEvent();
const positiveNumbers = numbers.filter({
fn: ({ x }) => x > 0,
});
const $lastPositive = createStore(0).on(positiveNumbers, (n, { x }) => x);
$lastPositive.watch((x) => {
console.log("last positive:", x);
});
// => last positive: 0
numbers({ x: 0 });
// no reaction
numbers({ x: -10 });
// no reaction
numbers({ x: 10 });
// => last positive: 10
Meaningful example
Let’s assume a standard situation when you want to buy sneakers in the shop, but there is no size. You subscribe to the particular size of the sneakers’ model, and in addition, you want to receive a notification if they have it, and ignore any other notification. Therefore, filtering can be helpful for that. Event filtering works in the same way. If filter
returns true
, the event will be called.
const sneackersReceived = createEvent<Sneakers>();
const uniqueSizeReceived = sneackersReceived.filter({
fn: (sneackers) => sneackers.size === 48,
});
filterMap(fn)
This methods generates a new derived ReadonlyEvent that may be invoked after the original event, but with the transformed argument. This special method enabled you to simultaneously transform data and filter out trigger of the event.
This method looks like the .filter()
and .map()
merged in the one. That’s it. The reason for creating was an impossibility for event filtering.
This method is mostly useful with JavaScript APIs whose returns undefined
sometimes.
Formulae
const second = first.filterMap(fn);
- When
first
is triggered, callfn
with payload fromfirst
- If
fn()
returnedundefined
do not triggersecond
- If
fn()
returned some data, triggersecond
with data fromfn()
Arguments
fn
(Function): A function that receivesargument
, should be pure.
The fn
function should return some data. When undefined
is returned, the update of derived event will be skipped.
Returns
ReadonlyEvent: The new event
Types
The type for the derived event is automatically inferred from the fn
declaration. No need to explicitly set type for variable or generic type argument:
import { createEvent } from "effector";
const first = createEvent<number>();
// first: Event<number>
const second = first.filterMap((count) => {
if (count === 0) return;
return count.toString();
});
// second: ReadonlyEvent<string>
The first
event can be represented as either Event<T>
or ReadonlyEvent<T>
.
The second
event will always be represented as ReadonlyEvent<T>
.
Example
import { createEvent } from "effector";
const listReceived = createEvent<string[]>();
// Array.prototype.find() returns `undefined` when no item is found
const effectorFound = listReceived.filterMap((list) => list.find((name) => name === "effector"));
effectorFound.watch((name) => console.info("found", name));
listReceived(["redux", "effector", "mobx"]); // => found effector
listReceived(["redux", "mobx"]);
Meaningful example
Consider a scenario where you walk into a grocery store with a specific task: you need to purchase 10 apples, but only if they’re red. If they’re not red, you’re out of luck. Let’s consider by steps:
- Take one apple;
- Have a look, is it red(put in a pack) or not(take another).
And you repeat this until you complete the task. Now think about it in the effector terms, and we consider the positive case:
- Take an apple – event;
- Have a look, red or no – filter;
- You keep it – map;
- Put in pack – event.
- Pack – store
watch(watcher)
This method enables you to call callback on each event trigger with the argument of the event.
The watch
method neither handles nor reports exceptions, manages the completion of asynchronous operations, nor addresses data race issues.
Its primary intended use is for short-term debugging and logging purposes.
Read more in the explanation section.
Formulae
const unwatch = event.watch(fn);
- The
fn
will be called on eachevent
trigger, passed argument of theevent
to thefn
. - When
unwatch
is called, stop callingfn
on eachevent
trigger.
Arguments
watcher
(Watcher): A function that receivesargument
from the event.
Returns
Subscription: Unsubscribe function.
Example
import { createEvent } from "effector";
const sayHi = createEvent();
const unwatch = sayHi.watch((name) => console.log(`${name}, hi there!`));
sayHi("Peter"); // => Peter, hi there!
unwatch();
sayHi("Drew"); // => nothing happened
subscribe(observer)
This is the low-level method to integrate event with the standard Observable
pattern.
You don’t need to use this method on your own. It is used under the hood by rendering engines or so on.
Read more:
Properties
These set of property is mostly set by effector/babel-plugin
or @effector/swc-plugin
. So they are exist only when babel or SWC is used.
sid
It is an unique identifier for each event.
It is important to note, SID is not changes on each app start, it is statically written inside your app bundle to absolutely identify units.
It can be useful to send events between workers or server/browser: examples/worker-rpc.
It has the string | null
type.
shortName
It is a string
type property, contains the name of the variable event declared at.
import { createEvent } from "effector";
const demo = createEvent();
// demo.shortName === 'demo'
But reassign event to another variable changes nothing:
const another = demo;
// another.shortName === 'demo'
compositeName
This property contains the full internal chain of units. For example, event can be created by the domain, so the composite name will contain a domain name inside it.
import { createEvent, createDomain } from "effector";
const first = createEvent();
const domain = createDomain();
const second = domain.createEvent();
console.log(first);
// => { shortName: "first", fullName: "first", path: ["first"] }
console.log(second);
// => { shortName: "second", fullName: "domain/second", path: ["domain", "second"] }