eslint-plugin-effector

website·Indexed 34 days ago

https://eslint.effector.dev

An 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.

Tokens
11.5K
Snippets
67
Records
79
Agent score
56%

What's inside eslint-effector

  1. Use the recommended ESLint preset for Effector

    The recommended preset provides a default set of ESLint rules designed to enforce consistent best practices across all Effector projects.
  2. 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-eslint and has updated environment requirements.
    # Minimum Node.js version requirements:
    # ^20.9.0 || >=21.1.0
    
    # Supported ESLint versions:
    # ^8.57.0 || ^9.0.0
  3. Replace effector forward with sample for better extensibility

    The effector/no-forward rule suggests replacing forward calls with sample calls. While forward simply pipes an event to another, sample is more flexible and extendable, allowing for data transformation via the fn property and conditional execution via the filter property.
    // 👎 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
    })
  4. 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 useUnit at the call site if the receiver's signature explicitly declares the parameter as a unit type (e.g., EventCallable or Effect). In this case, the receiver is responsible for binding the unit to the scope using useUnit internally.

    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 with useUnit before 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} />
  5. Replace effector guard with sample for better extensibility

    The effector/no-guard rule suggests replacing guard calls with sample calls. While guard is used for conditional triggering, sample provides the same functionality but is more extendable, specifically allowing the addition of a transformation via the fn property.
    // 👎 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,
    })
  6. Ensure persist calls include a pickup event for Scope support

    When using Scopes in Effector, every persist call from the effector-storage library must include a pickup event. This is necessary to ensure that the correct initial value is loaded into the store for each specific Scope. Without the pickup event, 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 
    })
  7. Simplify Effector guards by removing unnecessary combine/merge calls

    The effector/no-unnecessary-combination ESLint rule identifies cases where combine or merge are used inside a clock or source property (such as in a guard operator) when they can be replaced by simpler syntax. Instead of explicitly calling combine, 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,
    })
  8. 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.0 to match ESLint requirements. It supports ESLint versions ^8.57.0 || ^9.0.0.
  9. Prevent mixing Effect calls and raw async functions in Effect handlers

    To avoid losing Scope in the Fork API, an Effect handler must either call other Effects or perform raw asynchronous computations, but it cannot do both. If an Effect handler calls another Effect, any additional asynchronous logic (like setTimeout or Promise wrappers) must be wrapped in its own Effect.
    // ✅ 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()
    })
    
  10. Enforce Effector Effect naming conventions

    The effector/enforce-effect-naming-convention ESLint rule ensures that all Effector Effects are consistently named with an Fx suffix. This reduces code reading overhead by allowing developers to distinguish effects from regular variables or functions at a glance. For example, fetchUserInfoFx is a valid name for an effect, whereas fetchUserInfo is 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);
  11. Avoid using domain methods to create Effector units

    The effector/no-domain-unit-creators rule 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 the effector package and pass the domain as 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 })
  12. Simplify combine and merge calls in guard clock or source

    When using combine or merge within the clock or source properties of an Effector guard, the explicit call to combine or merge is 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,
    })