Splitting Data Streams with split
The split
method is designed to divide logic into multiple data streams.
For example, you might need to route data differently depending on its content, much like a railway switch that directs trains to different tracks:
- If a form is filled incorrectly β display an error.
- If everything is correct β send a request.
Conditions in split
are checked sequentially from top to bottom. Once a condition matches, subsequent ones are not evaluated. Keep this in mind when crafting your conditions.
Basic Usage of split
Letβs look at a simple example β processing messages of different types:
import { createEvent, split } from "effector";
const updateUserStatus = createEvent();
const { activeUserUpdated, idleUserUpdated, inactiveUserUpdated } = split(updateUserStatus, { activeUserUpdated: (userStatus) => userStatus === "active", idleUserUpdated: (userStatus) => userStatus === "idle", inactiveUserUpdated: (userStatus) => userStatus === "inactive",});
The logic here is straightforward. When the updateUserStatus
event is triggered, it enters split
, which evaluates each condition from top to bottom until a match is found, then triggers the corresponding event in effector
.
Each condition is defined by a predicate β a function returning true
or false
.
You might wonder, βWhy use this when I could handle conditions with if/else
in the UI?β The answer lies in Effectorβs philosophy of separating business logic from the UI.
Think of split
as a reactive switch
for units.
Default Case
When using split
, there might be situations where no conditions match. For such cases, thereβs a special default case: __
.
Hereβs the same example as before, now including a default case:
import { createEvent, split } from "effector";
const updateUserStatus = createEvent();
const { activeUserUpdated, idleUserUpdated, inactiveUserUpdated, __ } = split(updateUserStatus, { activeUserUpdated: (userStatus) => userStatus === "active", idleUserUpdated: (userStatus) => userStatus === "idle", inactiveUserUpdated: (userStatus) => userStatus === "inactive",});
__.watch((defaultStatus) => console.log("default case with status:", defaultStatus));activeUserUpdated.watch(() => console.log("active user"));
updateUserStatus("whatever");updateUserStatus("active");updateUserStatus("default case");
// Console output:// default case with status: whatever// active user// default case with status: default case
If no conditions match, the default case __
will be triggered.
Short Form
The split
method supports multiple usage patterns based on your needs.
The shortest usage involves passing a unit as the first argument serving as a trigger and an object with cases as the second argument.
Letβs look at an example with GitHubβs βStarβ and βWatchβ buttons:


import { createStore, createEvent, split } from "effector";
type Repo = { // ... other properties isStarred: boolean; isWatched: boolean;};
const toggleStar = createEvent<string>();const toggleWatch = createEvent<string>();
const $repo = createStore<null | Repo>(null) .on(toggleStar, (repo) => ({ ...repo, isStarred: !repo.isStarred, })) .on(toggleWatch, (repo) => ({ ...repo, isWatched: !repo.isWatched }));
const { starredRepo, unstarredRepo, __ } = split($repo, { starredRepo: (repo) => repo.isStarred, unstarredRepo: (repo) => !repo.isStarred,});
// Debug default case__.watch((repo) => console.log("[split toggleStar] Default case triggered with value ", repo));
// Somewhere in the apptoggleStar();
This usage returns an object with derived events, which can trigger reactive chains of actions.
Use this pattern when:
- There are no dependencies on external data (e.g., stores).
- You need simple, readable code.
Expanded Form
Using the split
method in this variation doesnβt return any value but provides several new capabilities:
- You can depend on external data, such as stores, using the
match
parameter. - Trigger multiple units when a case matches by passing an array.
- Add a data source using
source
and a trigger usingclock
.
For example, imagine a scenario where your application has two modes: user
and admin
. When an event is triggered, different actions occur depending on whether the mode is user
or admin
:
import { createStore, createEvent, createEffect, split } from "effector";
const adminActionFx = createEffect();const secondAdminActionFx = createEffect();const userActionFx = createEffect();const defaultActionFx = createEffect();// UI eventconst buttonClicked = createEvent();
// Current application modeconst $appMode = createStore<"admin" | "user">("user");
// Different actions for different modessplit({ source: buttonClicked, match: $appMode, // Logic depends on the current mode cases: { admin: [adminActionFx, secondAdminActionFx], user: userActionFx, __: defaultActionFx, },});
// Clicking the same button performs different actions// depending on the application modebuttonClicked();// -> "Performing user action" (when $appMode = 'user')// -> "Performing admin action" (when $appMode = 'admin')
Additionally, you can include a clock
property that works like in sample
, acting as a trigger, while source
provides the data to be passed into the respective case. Hereβs an extended example:
// Extending the previous code
const adminActionFx = createEffect((currentUser) => { // ...});const secondAdminActionFx = createEffect((currentUser) => { // ...});
// Adding a new storeconst $currentUser = createStore({ id: 1, name: "Donald",});
const $appMode = createStore<"admin" | "user">("user");
split({ clock: buttonClicked, // Passing the new store as a data source source: $currentUser, match: $appMode, cases: { admin: [adminActionFx, secondAdminActionFx], user: userActionFx, __: defaultActionFx, },});
If you need a default case, you must explicitly define it in the cases
object, otherwise, it wonβ t be processed!
In this scenario, the logic for handling cases is determined at runtime based on $appMode
, unlike the earlier example where it was defined during split
creation.
When using match
, it can accept units, functions, or objects with specific constraints:
- Store: If using a store, it must store a string value.
- Function: If passing a function, it must return a string value and be pure.
- Object with stores: If passing an object of stores, each store must hold a boolean value.
- Object with functions: If passing an object of functions, each function must return a boolean value and be pure.
match
as a Store
When match
is a store, the value in the store is used as a key to select the corresponding case:
const $currentTab = createStore("home");
split({ source: pageNavigated, match: $currentTab, cases: { home: loadHomeDataFx, profile: loadProfileDataFx, settings: loadSettingsDataFx, },});
match
as a Function
When using a function for match
, it must return a string to be used as the case key:
const userActionRequested = createEvent<{ type: string; payload: any }>();
split({ source: userActionRequested, match: (action) => action.type, // The function returns a string cases: { update: updateUserDataFx, delete: deleteUserDataFx, create: createUserDataFx, },});
match
as an Object with Stores
When match
is an object of stores, each store must hold a boolean value. The case whose store contains true will execute:
const $isAdmin = createStore(false);const $isModerator = createStore(false);
split({ source: postCreated, match: { admin: $isAdmin, moderator: $isModerator, }, cases: { admin: createAdminPostFx, moderator: createModeratorPostFx, __: createUserPostFx, },});
match
as an Object with Functions
If using an object of functions, each function must return a boolean. The first case with a true
function will execute:
split({ source: paymentReceived, match: { lowAmount: ({ amount }) => amount < 100, mediumAmount: ({ amount }) => amount >= 100 && amount < 1000, highAmount: ({ amount }) => amount >= 1000, }, cases: { lowAmount: processLowPaymentFx, mediumAmount: processMediumPaymentFx, highAmount: processHighPaymentFx, },});
Ensure your conditions in match
are mutually exclusive. Overlapping conditions may cause unexpected behavior. Always verify the logic to avoid conflicts.
Practical Examples
Handling forms with split
const showFormErrorsFx = createEffect(() => { // Logic to display errors});const submitFormFx = createEffect(() => { // Logic to submit the form});
const submitForm = createEvent();
const $form = createStore({ name: "", email: "", age: 0,}).on(submitForm, (_, submittedForm) => ({ ...submittedForm }));// Separate store for errorsconst $formErrors = createStore({ name: "", email: "", age: "",}).reset(submitForm);
// Validate fields and collect errorssample({ clock: submitForm, source: $form, fn: (form) => ({ name: !form.name.trim() ? "Name is required" : "", email: !isValidEmail(form.email) ? "Invalid email" : "", age: form.age < 18 ? "Age must be 18+" : "", }), target: $formErrors,});
// Use split for routing based on validation resultssplit({ source: $formErrors, match: { hasErrors: (errors) => Object.values(errors).some((error) => error !== ""), }, cases: { hasErrors: showFormErrorsFx, __: submitFormFx, },});
Explanation:
Two effects are created: one to display errors and one to submit the form.
Two stores are defined: $form
for form data and $formErrors
for errors.
On form submission submitForm
, two things happen:
- Form data is updated in the
$form
store. - All fields are validated using
sample
, and errors are stored in$formErrors
.
The split
method determines the next step:
- If any field has an error β β display the errors.
- If all fields are valid β β submit the form.