eslint-plugin-effector
website·Indexed 34 days ago
https://eslint.effector.devAn ESLint plugin for Effector that provides a set of rules to enforce naming conventions, prevent imperative code patterns (such as .watch and .getState), and optimize the use of operators like sample, guard, and combine.
What's inside eslint-effector
- The recommended preset provides a default set of ESLint rules designed to enforce consistent best practices across all Effector projects.
Upgrade to eslint-plugin-effector v0.17.0
Version 0.17.0 introduces several breaking changes and core updates. It now requires type-aware linting with@typescript-eslintand has updated environment requirements.# Minimum Node.js version requirements: # ^20.9.0 || >=21.1.0 # Supported ESLint versions: # ^8.57.0 || ^9.0.0Replace effector forward with sample for better extensibility
Theeffector/no-forwardrule suggests replacingforwardcalls withsamplecalls. Whileforwardsimply pipes an event to another,sampleis more flexible and extendable, allowing for data transformation via thefnproperty and conditional execution via thefilterproperty.// 👎 Avoid using forward for simple piping forward({ from: trigger, to: reaction }) // 👍 Use sample instead sample({ clock: trigger, target: reaction }) // 👎 Avoid using .map() inside forward forward({ from: trigger.map((value) => value.length), to: reaction }) // 👍 Use sample with the fn property for transformations sample({ clock: trigger, fn: (value) => value.length, target: reaction })Handle Effector units in custom hooks and components
When passing an Effector unit (like an event or effect) to a custom hook or component, you do not need to wrap it in
useUnitat the call site if the receiver's signature explicitly declares the parameter as a unit type (e.g.,EventCallableorEffect). In this case, the receiver is responsible for binding the unit to the scope usinguseUnitinternally.If the receiver is typed as a plain function
(arg: T) => R, it is not considered 'effector-aware' because it makes no guarantee to bind the unit to a scope. To resolve this, either explicitly type the parameter as a unit or wrap the unit withuseUnitbefore passing it.import { useUnit, EventCallable } from 'effector-react' type Props = { event: EventCallable<void> } // Receiver takes responsibility for binding to Scope const PressButton = ({ event }: Props) => ( <button onClick={useUnit(event)}>click</button> ) // ✅ Valid: PressButton's `event` is typed as a Unit, so it can be passed directly const Page = () => <PressButton event={pressed} />Replace effector guard with sample for better extensibility
Theeffector/no-guardrule suggests replacingguardcalls withsamplecalls. Whileguardis used for conditional triggering,sampleprovides the same functionality but is more extendable, specifically allowing the addition of a transformation via thefnproperty.// 👎 Avoid using guard for simple filtering guard({ clock: trigger, source: $data, filter: Boolean, target: reaction }) // 👍 Use sample instead sample({ clock: trigger, source: $data, filter: Boolean, target: reaction }) // 👎 Avoid mapping in source when using guard guard({ clock: trigger, source: $data.map((data) => data.length), filter: Boolean, target: reaction, }) // 👍 Use sample with the 'fn' property for transformations sample({ clock: trigger, source: $data, filter: Boolean, fn: (data) => data.length, target: reaction, })Ensure persist calls include a pickup event for Scope support
When using Scopes in Effector, everypersistcall from theeffector-storagelibrary must include apickupevent. This is necessary to ensure that the correct initial value is loaded into the store for each specificScope. Without thepickupevent, persistence will not function correctly across different scopes.import { persist } from "effector-storage/query" import { createStore, createEvent } from "effector" const $store = createStore("example") const pickup = createEvent() // Correct usage: pickup is specified to support Scopes persist({ store: $store, pickup })Simplify Effector guards by removing unnecessary combine/merge calls
Theeffector/no-unnecessary-combinationESLint rule identifies cases wherecombineormergeare used inside aclockorsourceproperty (such as in aguardoperator) when they can be replaced by simpler syntax. Instead of explicitly callingcombine, you can pass an array of stores or an object mapping keys to stores, which Effector handles natively.// 👎 Avoid explicit combine in clock/source const badEventOne = guard({ clock: combine($store1, $store2), filter: $filter, }) const badEventTwo = guard({ clock: combine($store1, $store2, (store1, store2) => ({ x: store1, y: store2, })), filter: $filter, }) // 👍 Use arrays or objects for automatic combination const goodEventOne = guard({ clock: [$store1, $store2], filter: $filter }) const goodEventTwo = guard({ clock: { x: $store1, y: $store2 }, filter: $filter, })Upgrade requirements for eslint-plugin-effector v0.17.0+
Starting from version 0.17.0, the plugin requires type-aware linting with@typescript-eslint. The minimum Node.js version is^20.9.0 || >=21.1.0to match ESLint requirements. It supports ESLint versions^8.57.0 || ^9.0.0.Prevent mixing Effect calls and raw async functions in Effect handlers
To avoid losing Scope in the Fork API, anEffecthandler must either call otherEffects or perform raw asynchronous computations, but it cannot do both. If an Effect handler calls another Effect, any additional asynchronous logic (likesetTimeoutorPromisewrappers) must be wrapped in its ownEffect.// ✅ CORRECT: Effect with only raw async computations const delayFx = createEffect(async () => { await new Promise((rs) => setTimeout(rs, 80)) }) // ✅ CORRECT: Effect calling only other Effects const authUserFx = createEffect() const sendMessageFx = createEffect() const delayFx = createEffect(async () => { /* ... */ }) const sendWithAuthFx = createEffect(async () => { await authUserFx() await delayFx() await sendMessageFx() }) // ❌ INCORRECT: Mixing Effect calls and raw async functions const sendWithAuthFx = createEffect(async () => { await authUserFx() // WRONG: This raw promise causes context/scope loss for subsequent effects await new Promise((rs) => setTimeout(rs, 80)) await sendMessageFx() })Enforce Effector Effect naming conventions
Theeffector/enforce-effect-naming-conventionESLint rule ensures that all EffectorEffects are consistently named with anFxsuffix. This reduces code reading overhead by allowing developers to distinguish effects from regular variables or functions at a glance. For example,fetchUserInfoFxis a valid name for an effect, whereasfetchUserInfois not.// ✅ Correct: Effect names end with Fx const fetchUserFx = createEffect(); const { fetchUserFx, fetchPostFx } = useContext(ApiContext); // ❌ Incorrect: Effect names missing Fx suffix const fetchUser = createEffect(); const { fetchUser, fetchPost } = useContext(ApiContext);Avoid using domain methods to create Effector units
Theeffector/no-domain-unit-creatorsrule disallows calling methods like.createEvent(),.createStore(), and.createEffect()directly on a domain object. These method-based APIs are difficult for AST-aware plugins and tools to parse, which can cause compatibility issues. Instead, use the standard factory functions from theeffectorpackage and pass thedomainas an option. This approach is more consistent with the Effector API and ensures compatibility with AST-based tooling.import { createDomain, createEvent, createStore, createEffect } from "effector" const domain = createDomain() // 👎 Avoid: hard to parse & work with const event = domain.createEvent() const $store = domain.createStore("value") const effectFx = domain.createEffect() // 👍 Prefer: clean and consistent const event = createEvent({ domain }) const $store = createStore("value", { domain }) const effectFx = createEffect({ domain })Simplify combine and merge calls in guard clock or source
When usingcombineormergewithin theclockorsourceproperties of an Effectorguard, the explicit call tocombineormergeis unnecessary and can be omitted. Instead, you can pass an array or an object containing the stores directly. This reduces boilerplate and simplifies the source code.// 👎 Avoid explicit combine calls const badEventOne = guard({ clock: combine($store1, $store2), filter: $filter, }) const badEventTwo = guard({ clock: combine($store1, $store2, (store1, store2) => ({ x: store1, y: store2, })), filter: $filter, }) // 👍 Better: Use arrays or objects directly const goodEventOne = guard({ clock: [$store1, $store2], filter: $filter }) const goodEventTwo = guard({ clock: { x: $store1, y: $store2 }, filter: $filter, })