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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ coverage
docs/.vitepress/dist
docs/**/__*.md
docs/changelog.md
docs/.vitepress/cache
docs/.vitepress/cache
.idea
**/*.xml
1 change: 1 addition & 0 deletions config/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ module.exports = {
"effector/enforce-gate-naming-convention": "error",
"effector/mandatory-scope-binding": "error",
"effector/prefer-useUnit": "warn",
"effector/use-unit-destructuring": "warn",
},
};
145 changes: 145 additions & 0 deletions docs/rules/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.
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
"prefer-useUnit": require("./rules/prefer-useUnit/prefer-useUnit"),
"require-pickup-in-persist": require("./rules/require-pickup-in-persist/require-pickup-in-persist"),
"no-patronum-debug": require("./rules/no-patronum-debug/no-patronum-debug"),
"use-unit-destructuring": require("./rules/use-unit-destructuring/use-unit-destructuring"),
},
configs: {
recommended: require("./config/recommended"),
Expand Down
139 changes: 139 additions & 0 deletions rules/use-unit-destructuring/use-unit-destructuring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
const { createLinkToRule } = require("../../utils/create-link-to-rule");
module.exports = {
meta: {
type: "problem",
docs: {
description:
"Ensure destructured properties match the passed unit object/array",
category: "Best Practices",
recommended: true,
url: createLinkToRule("use-unit-destructuring"),
},
messages: {
unusedKey: 'Property "{{key}}" is passed but not destructured',
missingKey:
'Property "{{key}}" is destructured but not passed in the unit object',
implicitSubscription:
"Element at index {{index}} ({{name}}) is passed but not destructured, causing implicit subscription",
},
schema: [],
},
create(context) {
return {
CallExpression(node) {
// Search for useUnit
if (
node.callee.type !== "Identifier" ||
node.callee.name !== "useUnit" ||
node.arguments.length === 0
) {
return;
}

const argument = node.arguments[0];
const parent = node.parent;

if (parent.type !== "VariableDeclarator") {
return;
}

// Shape is Object-like
if (
argument.type === "ObjectExpression" &&
parent.id.type === "ObjectPattern"
) {
handleObjectPattern(context, argument, parent.id);
}

// Shape is Array-like
if (
argument.type === "ArrayExpression" &&
parent.id.type === "ArrayPattern"
) {
handleArrayPattern(context, argument, parent.id);
}
},
};
},
};

function handleObjectPattern(context, objectArgument, objectPattern) {
// Collect all keys from argument object
const argumentKeys = new Set(
objectArgument.properties
.filter(
(prop) => prop.type === "Property" && prop.key.type === "Identifier"
)
.map((prop) => prop.key.name)
);

// Collect destructured keys
const destructuredKeys = new Set(
objectPattern.properties
.filter(
(prop) => prop.type === "Property" && prop.key.type === "Identifier"
)
.map((prop) => prop.key.name)
);

// Check unused keys
for (const key of argumentKeys) {
if (!destructuredKeys.has(key)) {
context.report({
node: objectArgument,
messageId: "unusedKey",
data: { key },
});
}
}

// Check missing keys
for (const key of destructuredKeys) {
if (!argumentKeys.has(key)) {
context.report({
node: objectPattern,
messageId: "missingKey",
data: { key },
});
}
}
}

function handleArrayPattern(context, arrayArgument, arrayPattern) {
const argumentElements = arrayArgument.elements;
const destructuredElements = arrayPattern.elements;

// Check all array elements was destructured
const destructuredCount = destructuredElements.filter(
(el) => el !== null
).length;
const argumentCount = argumentElements.filter((el) => el !== null).length;

if (destructuredCount < argumentCount) {
// If undestructured elements exists
for (let i = destructuredCount; i < argumentCount; i++) {
const element = argumentElements[i];
if (element) {
// Get the name of variable for an info message
let name = "unknown";
if (element.type === "Identifier") {
name = element.name;
} else if (element.type === "MemberExpression") {
const sourceCode = context.getSourceCode
? context.getSourceCode()
: context.sourceCode;
name = sourceCode.getText(element);
}

context.report({
node: element,
messageId: "implicitSubscription",
data: {
index: i,
name: name,
},
});
}
}
}
}
1 change: 1 addition & 0 deletions rules/use-unit-destructuring/use-unit-destructuring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://eslint.effector.dev/rules/use-unit-destructuring.html
Loading