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
1 change: 1 addition & 0 deletions apps/website/content/docs/rules/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"no-missing-key",
"no-misused-capture-owner-stack",
"no-nested-component-definitions",
"no-nested-lazy-component-declarations",
"no-prop-types",
"no-redundant-should-component-update",
"no-set-state-in-component-did-mount",
Expand Down
1 change: 1 addition & 0 deletions apps/website/content/docs/rules/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Linter rules can have false positives, false negatives, and some rules are depen
| [`no-missing-key`](./no-missing-key) | 2️⃣ | | Disallow missing `key` on items in list rendering | |
| [`no-misused-capture-owner-stack`](./no-misused-capture-owner-stack) | 0️⃣ | `🧪` | Prevents incorrect usage of `captureOwnerStack` | |
| [`no-nested-component-definitions`](./no-nested-component-definitions) | 2️⃣ | | Disallow nesting component definitions inside other components | |
| [`no-nested-lazy-component-declarations`](./no-nested-lazy-component-declarations) | 2️⃣ | | Disallow nesting lazy component declarations inside other components | |
| [`no-prop-types`](./no-prop-types) | 2️⃣ | | Disallow `propTypes` in favor of TypeScript or another type-checking solution | |
| [`no-redundant-should-component-update`](./no-redundant-should-component-update) | 2️⃣ | | Disallow `shouldComponentUpdate` when extending `React.PureComponent` | |
| [`no-set-state-in-component-did-mount`](./no-set-state-in-component-did-mount) | 1️⃣ | | Disallow calling `this.setState` in `componentDidMount` outside of functions, such as callbacks | |
Expand Down
2 changes: 2 additions & 0 deletions packages/core/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
- [isForwardRef](variables/isForwardRef.md)
- [isForwardRefCall](variables/isForwardRefCall.md)
- [isInversePhase](variables/isInversePhase.md)
- [isLazy](variables/isLazy.md)
- [isLazyCall](variables/isLazyCall.md)
- [isMemo](variables/isMemo.md)
- [isMemoCall](variables/isMemoCall.md)
- [isUseActionStateCall](variables/isUseActionStateCall.md)
Expand Down
9 changes: 9 additions & 0 deletions packages/core/docs/variables/isLazy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[**@eslint-react/core**](../README.md)

***

[@eslint-react/core](../README.md) / isLazy

# Variable: isLazy

> `const` **isLazy**: `ReturnType`
9 changes: 9 additions & 0 deletions packages/core/docs/variables/isLazyCall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[**@eslint-react/core**](../README.md)

***

[@eslint-react/core](../README.md) / isLazyCall

# Variable: isLazyCall

> `const` **isLazyCall**: `ReturnType`
4 changes: 2 additions & 2 deletions packages/core/src/component/component-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function isValidComponentDefinition(context: RuleContext, node: AST.TSEST
if (hint & ComponentDetectionHint.SkipArrayMapArgument && AST.isArrayMapCall(node.parent)) {
return false;
}
const boundaryNode = AST.findParentNode(
const significantParent = AST.findParentNode(
node,
AST.isOneOf([
T.JSXExpressionContainer,
Expand All @@ -52,5 +52,5 @@ export function isValidComponentDefinition(context: RuleContext, node: AST.TSEST
T.ClassBody,
]),
);
return boundaryNode == null || boundaryNode.type !== T.JSXExpressionContainer;
return significantParent == null || significantParent.type !== T.JSXExpressionContainer;
}
2 changes: 2 additions & 0 deletions packages/core/src/utils/is-react-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const isCreateElement = isReactAPI("createElement");
export const isCreateRef = isReactAPI("createRef");
export const isForwardRef = isReactAPI("forwardRef");
export const isMemo = isReactAPI("memo");
export const isLazy = isReactAPI("lazy");

export const isCaptureOwnerStackCall = isReactAPICall("captureOwnerStack");
export const isChildrenCountCall = isReactAPICall("Children", "count");
Expand All @@ -45,3 +46,4 @@ export const isCreateElementCall = isReactAPICall("createElement");
export const isCreateRefCall = isReactAPICall("createRef");
export const isForwardRefCall = isReactAPICall("forwardRef");
export const isMemoCall = isReactAPICall("memo");
export const isLazyCall = isReactAPICall("lazy");
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
const { ctx, listeners } = ER.useComponentCollectorLegacy();
return {
...listeners,
"Program:exit"(node) {
const components = ctx.getAllComponents(node);
"Program:exit"(program) {
const components = ctx.getAllComponents(program);
for (const { name = "anonymous", node: component } of components.values()) {
context.report({
messageId: "classComponent",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
);
return {
...listeners,
"Program:exit"(node) {
const components = ctx.getAllComponents(node);
"Program:exit"(program) {
const components = ctx.getAllComponents(program);
for (const { name = "anonymous", node, displayName, flag, hookCalls } of components.values()) {
context.report({
messageId: "functionComponent",
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/eslint-plugin-react-debug/src/rules/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {

return {
...listeners,
"Program:exit"(node) {
const allHooks = ctx.getAllHooks(node);
"Program:exit"(program) {
const allHooks = ctx.getAllHooks(program);

for (const { name, node, hookCalls } of allHooks.values()) {
context.report({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ react-hooks-extra/no-unnecessary-use-callback

## Description

Disallows unnecessary usage of `useCallback`.
Disallow unnecessary usage of `useCallback`.

React Hooks `useCallback` has empty dependencies array like what's in the examples, are unnecessary. The hook can be removed and it's value can be created in the component body or hoisted to the outer scope of the component.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
const { ctx, listeners } = ER.useHookCollector();
return {
...listeners,
"Program:exit"(node) {
const allHooks = ctx.getAllHooks(node);
"Program:exit"(program) {
const allHooks = ctx.getAllHooks(program);
for (const { name, node, hookCalls } of allHooks.values()) {
// Skip empty functions
if (AST.isEmptyFunction(node)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ export function create(context: RuleContext<MessageID, Options>): RuleListener {
return {
...collector.listeners,
...collectorLegacy.listeners,
"Program:exit"(node) {
const functionComponents = collector.ctx.getAllComponents(node);
const classComponents = collectorLegacy.ctx.getAllComponents(node);
"Program:exit"(program) {
const functionComponents = collector.ctx.getAllComponents(program);
const classComponents = collectorLegacy.ctx.getAllComponents(program);
for (const { node: component } of functionComponents.values()) {
const id = AST.getFunctionIdentifier(component);
if (id?.name == null) continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,21 @@ export function create(context: RuleContext<MessageID, Options>): RuleListener {
JSXFragment() {
hasJSXNode = true;
},
"Program:exit"(node) {
"Program:exit"(program) {
const fileNameExt = filename.slice(filename.lastIndexOf("."));
const isJSXExt = extensions.includes(fileNameExt);
if (hasJSXNode && !isJSXExt) {
context.report({
messageId: "useJsxFileExtension",
node,
node: program,
data: {
extensions: extensionsString,
},
});
return;
}

const hasCode = node.body.length > 0;
const hasCode = program.body.length > 0;
const ignoreFilesWithoutCode = isObject(options) && options.ignoreFilesWithoutCode === true;
if (!hasCode && ignoreFilesWithoutCode) {
return;
Expand All @@ -127,7 +127,7 @@ export function create(context: RuleContext<MessageID, Options>): RuleListener {
) {
context.report({
messageId: "useNonJsxFileExtension",
node,
node: program,
data: {
extensions: extensionsString,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const rules = {
"react-x/no-missing-key": "error",
"react-x/no-misused-capture-owner-stack": "error",
"react-x/no-nested-component-definitions": "error",
"react-x/no-nested-lazy-component-declarations": "warn",
"react-x/no-prop-types": "error",
"react-x/no-redundant-should-component-update": "error",
"react-x/no-set-state-in-component-did-mount": "warn",
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/eslint-plugin-react-x/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import noMissingContextDisplayName from "./rules/no-missing-context-display-name
import noMissingKey from "./rules/no-missing-key";
import noMisusedCaptureOwnerStack from "./rules/no-misused-capture-owner-stack";
import noNestedComponentDefinitions from "./rules/no-nested-component-definitions";
import noNestedLazyComponentDeclarations from "./rules/no-nested-lazy-component-declarations";
import noPropTypes from "./rules/no-prop-types";
import noRedundantShouldComponentUpdate from "./rules/no-redundant-should-component-update";
import noSetStateInComponentDidMount from "./rules/no-set-state-in-component-did-mount";
Expand Down Expand Up @@ -91,6 +92,7 @@ export const plugin = {
"no-missing-key": noMissingKey,
"no-misused-capture-owner-stack": noMisusedCaptureOwnerStack,
"no-nested-component-definitions": noNestedComponentDefinitions,
"no-nested-lazy-component-declarations": noNestedLazyComponentDeclarations,
"no-prop-types": noPropTypes,
"no-redundant-should-component-update": noRedundantShouldComponentUpdate,
"no-set-state-in-component-did-mount": noSetStateInComponentDidMount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ react-x/no-access-state-in-setstate

## Description

Disallows accessing `this.state` inside `setState` calls.
Disallow accessing `this.state` inside `setState` calls.

Usage of `this.state` inside `setState` calls might result in errors when two state calls are called in batch and thus referencing old state and not the current state.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
const { ctx, listeners } = ER.useComponentCollectorLegacy();
return {
...listeners,
"Program:exit"(node) {
const components = ctx.getAllComponents(node);
"Program:exit"(program) {
const components = ctx.getAllComponents(program);
for (const { name = "anonymous", node: component } of components.values()) {
if (component.body.body.some((m) => ER.isComponentDidCatch(m) || ER.isGetDerivedStateFromError(m))) {
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {

return {
...listeners,
"Program:exit"(node) {
const components = ctx.getAllComponents(node);
"Program:exit"(program) {
const components = ctx.getAllComponents(program);
for (const { node: component } of components.values()) {
const { body } = component.body;
for (const member of body) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
const { ctx, listeners } = ER.useComponentCollectorLegacy();
return {
...listeners,
"Program:exit"(node) {
const components = ctx.getAllComponents(node);
"Program:exit"(program) {
const components = ctx.getAllComponents(program);
for (const { node: component } of components.values()) {
const { body } = component.body;
for (const member of body) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {

return {
...listeners,
"Program:exit"(node) {
const components = ctx.getAllComponents(node);
"Program:exit"(program) {
const components = ctx.getAllComponents(program);
for (const { node: component } of components.values()) {
const { body } = component.body;
for (const member of body) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
);
return {
...listeners,
"Program:exit"(node) {
const components = ctx.getAllComponents(node);
"Program:exit"(program) {
const components = ctx.getAllComponents(program);
for (const { node, displayName, flag } of components.values()) {
const isMemoOrForwardRef = (flag & (ER.ComponentFlag.ForwardRef | ER.ComponentFlag.Memo)) > 0n;
if (AST.getFunctionIdentifier(node) != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,17 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
return {
...collector.listeners,
...collectorLegacy.listeners,
"Program:exit"(node) {
"Program:exit"(program) {
const functionComponents = [
...collector
.ctx
.getAllComponents(node)
.getAllComponents(program)
.values(),
];
const classComponents = [
...collectorLegacy
.ctx
.getAllComponents(node)
.getAllComponents(program)
.values(),
];
const isFunctionComponent = (node: TSESTree.Node): node is AST.TSESTreeFunction => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: no-nested-lazy-component-declarations
---

**Full Name in `eslint-plugin-react-x@beta`**

```plain copy
react-x/no-nested-lazy-component-declarations
```

**Full Name in `@eslint-react/eslint-plugin@beta`**

```plain copy
@eslint-react/no-nested-lazy-component-declarations
```

**Presets**

- `x`
- `recommended`
- `recommended-typescript`
- `recommended-type-checked`

## Description

Disallow nesting lazy component declarations inside other components.

When a lazy component is declared inside another component, it will be re-created on every render of the parent component. This can lead to unexpected behavior, such as resetting the state of the lazy component.

## Examples

### Failing

```tsx
import { lazy } from "react";

function Editor() {
// 🔴 Bad: This will cause all state to be reset on re-renders
const MarkdownPreview = lazy(() => import("./MarkdownPreview.js"));
// ^^^^^^^^^^^^^^^
// - Do not declare lazy components inside other components. Instead, always declare them at the top level of your module.
// ...
}
```

### Passing

```tsx
import { lazy } from "react";

// ✅ Good: Declare lazy components outside of your components
const MarkdownPreview = lazy(() => import("./MarkdownPreview.js"));

function Editor() {
// ...
}
```

## Implementation

- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-nested-lazy-component-declarations.ts)
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-nested-lazy-component-declarations.spec.ts)

## Further Reading

- [React: Nesting and organizing components](https://react.dev/learn/your-first-component#nesting-and-organizing-components)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import tsx from "dedent";

import { allValid, ruleTester } from "../../../../../test";
import rule, { RULE_NAME } from "./no-nested-lazy-component-declarations";

ruleTester.run(RULE_NAME, rule, {
invalid: [
{
code: tsx`
import { lazy } from "react";

function Editor() {
// 🔴 Bad: This will cause all state to be reset on re-renders
const MarkdownPreview = lazy(() => import("./MarkdownPreview.js"));
// ^^^^^^^^^^^^^^^
// - Do not declare lazy components inside other components. Instead, always declare them at the top level of your module.
// ...

return null;
}
`,
errors: [
{
messageId: "noNestedComponentDefinitions",
},
],
},
],
valid: [
...allValid,
tsx`
import { lazy } from "react";

// ✅ Good: Declare lazy components outside of your components
const MarkdownPreview = lazy(() => import("./MarkdownPreview.js"));

function Editor() {
// ...
}
`,
],
});
Loading