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 caseIf 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 matchparameter.
- Trigger multiple units when a case matches by passing an array.
- Add a data source using sourceand 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 $formstore.
- 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.