Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.idea
**/*.xml

# Logs
logs
*.log
Expand Down
Empty file added config/react.js
Empty file.
Empty file added index.js
Empty file.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import noWatch from "./rules/no-watch/no-watch"
import preferUseUnit from "./rules/prefer-useUnit/prefer-useUnit"
import requirePickupInPersist from "./rules/require-pickup-in-persist/require-pickup-in-persist"
import strictEffectHandlers from "./rules/strict-effect-handlers/strict-effect-handlers"
import useUnitDestructuring from "./rules/use-unit-destructuring/use-unit-destructuring"
import { ruleset } from "./ruleset"

const base = {
Expand All @@ -45,6 +46,7 @@ const base = {
"no-useless-methods": noUselessMethods,
"no-watch": noWatch,
"prefer-useUnit": preferUseUnit,
"use-unit-destructuring": useUnitDestructuring,
"require-pickup-in-persist": requirePickupInPersist,
"strict-effect-handlers": strictEffectHandlers,
},
Expand Down
145 changes: 145 additions & 0 deletions src/rules/use-unit-destructuring/use-unit-destructuring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# effector/use-unit-destructuring

[Related documentation](https://effector.dev/en/api/effector-react/useunit/)

Ensures that all units passed to useUnit are properly destructured to avoid unused subscriptions and implicit re-renders.

## Rule Details
This rule enforces that:
- All properties passed in an object to useUnit must be destructured to prevent implicit subscriptions;
- All elements passed in an array to useUnit must be destructured to prevent implicit subscriptions also.

### Object shape
When using useUnit with an object, you must destructure all keys that you pass. Otherwise, unused units will still create subscriptions and cause unnecessary re-renders.
TypeScript

```ts
// 👍 correct - all properties are destructured
const { value, setValue } = useUnit({
value: $store,
setValue: event,
});
```

```ts
// 👎 incorrect - setValue is not destructured but still creates subscription
const { value } = useUnit({
value: $store,
setValue: event, // unused but subscribed!
});
```

```ts
// 👎 incorrect - extra is destructured but not passed
const {
value,
setValue,
extra // extra is missing - will be undefined
} = useUnit({
value: $store,
setValue: event,
});
```

### Array shape
When using useUnit with an array, you must destructure all elements. Elements that are not destructured will still create subscriptions, leading to implicit re-renders.
TypeScript

```ts
// 👍 correct - all elements are destructured
const [value, setValue] = useUnit([$store, event]);
```

```ts
// 👎 incorrect - $store is not destructured but creates implicit subscription
const [setValue] = useUnit([event, $store]);
// Component will re-render when $store changes, even though you don't use it!
```

```ts
// 👎 incorrect - event and $anotherStore cause implicit subscriptions
const [value] = useUnit([$store, event, $anotherStore]);
// Component re-renders on $store, event, and $anotherStore changes
```

## Why is this important?
Implicit subscriptions can lead to:
- Performance issues: unnecessary re-renders when unused stores update
- Hard-to-debug behavior: component re-renders for unclear reasons
- Memory leaks: subscriptions that are never cleaned up properly

## Examples

### Real-world example

```tsx
import React, { Fragment } from "react";
import { createEvent, createStore } from "effector";
import { useUnit } from "effector-react";

const $store = createStore("Hello World!");
const event = createEvent();

// 👎 incorrect
const BadComponent = () => {
const { value } = useUnit({
value: $store,
setValue: event, // ❌ not used but subscribed!
});

return <Fragment>{value}</Fragment>;
};

// 👍 correct
const GoodComponent = () => {
const { value, setValue } = useUnit({
value: $store,
setValue: event,
});

return <button onClick={() => setValue("New value")}>{value}</button>;
};
```

```tsx
import React, { Fragment } from "react";
import { createEvent, createStore } from "effector";
import { useUnit } from "effector-react";

const $store = createStore("Hello World!");
const event = createEvent();

// 👎 incorrect - implicit subscription to $store
const BadComponent = () => {
const [setValue] = useUnit([event, $store]); // ❌ $store not used but subscribed!

return <button onClick={() => setValue("New value")}>Click</button>;
};

// 👍 correct - explicit destructuring
const GoodComponent = () => {
const [value, setValue] = useUnit([$store, event]);

return <button onClick={() => setValue("New value")}>{value}</button>;
};

// 👍 also correct - only pass what you need
const AlsoGoodComponent = () => {
const [setValue] = useUnit([event]); // ✅ no implicit subscriptions

return <button onClick={() => setValue("New value")}>Click</button>;
};
```

### When Not To Use It
If you intentionally want to subscribe to a store without using its value (rare case), you can disable this rule for that line:

```tsx
// eslint-disable-next-line effector/use-unit-destructuring
const { value } = useUnit({
value: $store,
trigger: $triggerStore, // intentionally subscribing without using
});
```

However, in most cases, you should refactor your code to avoid implicit subscriptions.
165 changes: 165 additions & 0 deletions src/rules/use-unit-destructuring/use-unit-destructuring.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { RuleTester } from "@typescript-eslint/rule-tester"

import rule from "./use-unit-destructuring"

const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: { jsx: true },
},
},
})

ruleTester.run("effector/use-unit-destructuring", rule, {
valid: [
// All keys were destructured
{
code: `
import { useUnit } from "effector-react";
const { value, setValue } = useUnit({
value: $store,
setValue: event,
});
`,
},
// All keys were destructured
{
code: `
import { useUnit } from "effector-react";
const [value, setValue] = useUnit([$store, event]);
`,
},
// With one element in object-shape
{
code: `
import { useUnit } from "effector-react";
const { value } = useUnit({ value: $store });
`,
},
// With one element in array-shape
{
code: `
import { useUnit } from "effector-react";
const [value] = useUnit([$store]);
`,
},
// Is not useUnit - no check
{
code: `
const { value } = someOtherFunction({
value: $store,
setValue: event,
});
`,
},
],

invalid: [
// Object: not destructured
{
code: `
import { useUnit } from "effector-react";
const { value } = useUnit({
value: $store,
setValue: event,
});
`,
errors: [
{
messageId: "unusedKey",
data: { key: "setValue" },
},
],
},
// Object: destructured, but key does not exist
{
code: `
import { useUnit } from "effector-react";
const { value, setValue, extra } = useUnit({
value: $store,
setValue: event,
});
`,
errors: [
{
messageId: "missingKey",
data: { key: "extra" },
},
],
},
// Array: implicit subscription (not all elements were destructuring)
{
code: `
import { useUnit } from "effector-react";
const [setValue] = useUnit([event, $store]);
`,
errors: [
{
messageId: "implicitSubscription",
data: { index: 1, name: "$store" },
},
],
},
// Array: several implicit subscriptions
{
code: `
import { useUnit } from "effector-react";
const [value] = useUnit([$store, event, $anotherStore]);
`,
errors: [
{
messageId: "implicitSubscription",
data: { index: 1, name: "event" },
},
{
messageId: "implicitSubscription",
data: { index: 2, name: "$anotherStore" },
},
],
},
// Object: several unused keys
{
code: `
import { useUnit } from "effector-react";
const { value } = useUnit({
value: $store,
setValue: event,
reset: resetEvent,
});
`,
errors: [
{
messageId: "unusedKey",
data: { key: "setValue" },
},
{
messageId: "unusedKey",
data: { key: "reset" },
},
],
},
// JSX component with object-shape
{
code: `
import React, { Fragment } from "react";
import { useUnit } from "effector-react";

const ObjectShapeComponent = () => {
const { value } = useUnit({
value: $store,
setValue: event,
});
return <Fragment>{value}</Fragment>;
};
`,
errors: [
{
messageId: "unusedKey",
data: { key: "setValue" },
},
],
},
],
})
Loading