Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/yummy-words-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-effector": patch
---

Tighten `mandatory-scope-binding` to only flag potential unit invocations
3 changes: 2 additions & 1 deletion src/rules/mandatory-scope-binding/fixtures/model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createEffect, createEvent } from "effector"

export const clicked = createEvent<unknown>()
export const mounted = createEvent<void>()
export const fetchFx = createEffect(() => {})

export const $$ = { context: { outputs: { clicked } } }
export const $$ = { context: { outputs: { clicked, mounted } } }
27 changes: 23 additions & 4 deletions src/rules/mandatory-scope-binding/mandatory-scope-binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,40 @@ description: Forbid Event and Effect usage without useUnit in React components

# effector/mandatory-scope-binding

Forbids `Event` and `Effect` usage without `useUnit` in React components.
This ensures `Fork API` compatibility and allows writing isomorphic code for SSR apps.
Forbids `EventCallable` and `Effect` usage without `useUnit` in React. This ensures `Fork API` compatibility for easy testing via `fork()` and running isomorphic code in SSR/SSG apps.

```tsx
const increment = createEvent()

// 👍 Event usage is wrapped with `useUnit`
const GoodButton = () => {
const incrementEvent = useUnit(increment)
const onClick = useUnit(increment)

return <button onClick={incrementEvent}>+</button>
return <button onClick={onClick /* bound to Scope */}>+</button>
}

// 👎 Event is not wrapped with `useUnit` - component is not suitable for isomorphic SSR app
const BadButton = () => {
return <button onClick={increment}>+</button>
}
```

This rule doesn't enforce using a `Scope` by itself – your app will run scopeless unless configured. However, when you do, `mandatory-scope-binding` rule ensures no additional work needed to ensure `Scope` is not lost.

### Custom Hooks and Components

You don't need `useUnit` everywhere – passing a unit straight to a custom `effector` aware hook or component whose signature openly declares a unit-typed parameter is fine. It is assumed the receiver takes responsibility of binding event to `Scope` via `useUnit`.

```tsx
type Props = { event: EventCallable<void> }
const PressButton = ({ event }: Props) => <button onClick={useUnit(event) /* <== bound to Scope */}>click</button>

// 👍 PressButton's `event` is typed as a Unit – just pass it in, no issue
const Page = () => <PressButton event={pressed} />
```

::: warning Receiver Type Guarantee
A receiver typed as a plain function `(arg: T) => R` does not qualify as `effector`-aware. TypeScript's structural typing allows units to satisfy such signatures, but the receiver makes no promise to bind the unit to a `Scope`.

To fix this, either type the parameter explicitly as a unit (`EventCallable` / `Effect`) to signal that the receiver is responsible for scope binding, or wrap the unit with `useUnit` at the call site.
:::
Loading
Loading