Skip to content

Commit 2b4db09

Browse files
authored
refactor: Improve rule react/no-unstable-nested-components, make its behavior closer to facebook/react#25360 (#255)
1 parent 38da7e5 commit 2b4db09

File tree

3 files changed

+67
-164
lines changed

3 files changed

+67
-164
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### 🪄 Improvements
44

55
- Add `react/no-clone-element` to `recommended` and `recommended-legacy` presets.
6+
- Improve rule `react/no-unstable-nested-components`, make its behavior closer to [react-hooks/no-nested-components](https://github.com/facebook/react/pull/25360).
67

78
## v0.10.0 (Thu Dec 21 2023)
89

packages/plugins/eslint-plugin-react/src/rules/no-unstable-nested-components.spec.ts

Lines changed: 64 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -47,63 +47,6 @@ ruleTester.run(RULE_NAME, rule, {
4747
});
4848
}
4949
`,
50-
dedent`
51-
function ParentComponent() {
52-
const MemoizedNestedComponent = React.useCallback(() => <div />, []);
53-
54-
return (
55-
<div>
56-
<MemoizedNestedComponent />
57-
</div>
58-
);
59-
}
60-
`,
61-
dedent`
62-
function ParentComponent() {
63-
const MemoizedNestedComponent = React.useCallback(
64-
() => React.createElement("div", null),
65-
[]
66-
);
67-
68-
return React.createElement(
69-
"div",
70-
null,
71-
React.createElement(MemoizedNestedComponent, null)
72-
);
73-
}
74-
`,
75-
dedent`
76-
function ParentComponent() {
77-
const MemoizedNestedFunctionComponent = React.useCallback(
78-
function () {
79-
return <div />;
80-
},
81-
[]
82-
);
83-
84-
return (
85-
<div>
86-
<MemoizedNestedFunctionComponent />
87-
</div>
88-
);
89-
}
90-
`,
91-
dedent`
92-
function ParentComponent() {
93-
const MemoizedNestedFunctionComponent = React.useCallback(
94-
function () {
95-
return React.createElement("div", null);
96-
},
97-
[]
98-
);
99-
100-
return React.createElement(
101-
"div",
102-
null,
103-
React.createElement(MemoizedNestedFunctionComponent, null)
104-
);
105-
}
106-
`,
10750
dedent`
10851
function ParentComponent(props) {
10952
// Should not interfere handler declarations
@@ -402,69 +345,6 @@ ruleTester.run(RULE_NAME, rule, {
402345
}
403346
}
404347
`,
405-
dedent`
406-
function App({ locale }: AppProps) {
407-
const route = Router.useRoute(["Home", "BotArea", "NotFound"]);
408-
409-
return (
410-
<TypesafeI18n locale={locale}>
411-
<MantineProvider theme={mantineTheme}>
412-
<div className={css.root}>
413-
<React.Suspense fallback={<RootLayout navHeader={<small className={css.loading} />} />}>
414-
{React.useMemo(
415-
() => match(route)
416-
.with({ name: "Home" }, () => <Redirect to="/bots/ChatGPT" />)
417-
.with({ name: "BotArea" }, ({ params }) => <BotArea botName={params.botName} />)
418-
.otherwise(() => <NotFound />),
419-
[loaded, route],
420-
)}
421-
</React.Suspense>
422-
</div>
423-
</MantineProvider>
424-
</TypesafeI18n>
425-
);
426-
}
427-
`,
428-
dedent`
429-
function BotArea({ botName }: BotAreaProps) {
430-
const bot = useAtomValue(botsDb.item(botName));
431-
const route = Router.useRoute(["BotRoot", "BotChat", "BotNewChat", "BotSettings"]);
432-
const botList = useAtomValue(botListAtom);
433-
434-
const contentView = React.useMemo(
435-
() =>
436-
match(route)
437-
.with({ name: "BotRoot" }, ({ params }) => <RedirectChat botName={params.botName} />)
438-
.with({ name: "BotNewChat" }, ({ params }) => <RedirectChat botName={params.botName} />)
439-
.with({ name: "BotSettings" }, ({ params }) => <BotSettings botName={params.botName} />)
440-
.with({ name: "BotChat" }, ({ params }) => {
441-
const { botName, chatID } = params;
442-
443-
if (!ID.isChatID(chatID)) {
444-
return <Redirect to="/404" />;
445-
}
446-
447-
return <ChatDetail botName={botName} chatID={chatID} />;
448-
})
449-
.otherwise(() => null),
450-
[route],
451-
);
452-
453-
if (!bot) {
454-
return <Redirect to="/404" />;
455-
}
456-
457-
return (
458-
<BotProvider botName={botName}>
459-
<RootLayout nav={<BotList items={botList} selected={botName} />}>
460-
<ErrorBoundary fallback={<p className="p-2">Failed to render bot area.</p>}>
461-
<React.Suspense>{contentView}</React.Suspense>
462-
</ErrorBoundary>
463-
</RootLayout>
464-
</BotProvider>
465-
);
466-
}
467-
`,
468348
dedent`
469349
function ComponentWithProps(props) {
470350
return <div />;
@@ -1007,45 +887,72 @@ ruleTester.run(RULE_NAME, rule, {
1007887
},
1008888
{
1009889
code: dedent`
1010-
export function BotList({ items, selected }: BotListProps) {
1011-
return (
1012-
<div className={css.root}>
1013-
{items.map((item) => {
1014-
return match(item)
1015-
.when(
1016-
({ id }) => id === selected,
1017-
({ id, title, icon }) => (
1018-
<BotMenu botName={title} key={id}>
1019-
<Button
1020-
aria-label="bot-button"
1021-
render={<button type="button" />}
1022-
clickOnEnter
1023-
clickOnSpace
1024-
>
1025-
<Avatar bg={icon} />
1026-
</Button>
1027-
</BotMenu>
1028-
),
1029-
)
1030-
.otherwise(({ id, title, icon }) => (
1031-
<Link key={id} to={\`/bots/\${title}\`}>
1032-
<Avatar bg={icon} />
1033-
</Link>
1034-
));
1035-
})}
1036-
<Indicator label="WIP" size={14} inline>
1037-
<div className={css.plus}>
1038-
<Icon as={Plus} color={vars.colors.overlay} size={24} />
1039-
</div>
1040-
</Indicator>
1041-
</div>
1042-
);
890+
function ParentComponent() {
891+
const MemoizedNestedComponent = React.useCallback(() => <div />, []);
892+
893+
return (
894+
<div>
895+
<MemoizedNestedComponent />
896+
</div>
897+
);
1043898
}
1044899
`,
1045-
errors: [
1046-
{ messageId: "UNSTABLE_NESTED_COMPONENT" },
1047-
{ messageId: "UNSTABLE_NESTED_COMPONENT" },
1048-
],
900+
errors: [{ messageId: "UNSTABLE_NESTED_COMPONENT" }],
901+
},
902+
{
903+
code: dedent`
904+
function ParentComponent() {
905+
const MemoizedNestedComponent = React.useCallback(
906+
() => React.createElement("div", null),
907+
[]
908+
);
909+
910+
return React.createElement(
911+
"div",
912+
null,
913+
React.createElement(MemoizedNestedComponent, null)
914+
);
915+
}
916+
`,
917+
errors: [{ messageId: "UNSTABLE_NESTED_COMPONENT" }],
918+
},
919+
{
920+
code: dedent`
921+
function ParentComponent() {
922+
const MemoizedNestedFunctionComponent = React.useCallback(
923+
function () {
924+
return <div />;
925+
},
926+
[]
927+
);
928+
929+
return (
930+
<div>
931+
<MemoizedNestedFunctionComponent />
932+
</div>
933+
);
934+
}
935+
`,
936+
errors: [{ messageId: "UNSTABLE_NESTED_COMPONENT" }],
937+
},
938+
{
939+
code: dedent`
940+
function ParentComponent() {
941+
const MemoizedNestedFunctionComponent = React.useCallback(
942+
function () {
943+
return React.createElement("div", null);
944+
},
945+
[]
946+
);
947+
948+
return React.createElement(
949+
"div",
950+
null,
951+
React.createElement(MemoizedNestedFunctionComponent, null)
952+
);
953+
}
954+
`,
955+
errors: [{ messageId: "UNSTABLE_NESTED_COMPONENT" }],
1049956
},
1050957
],
1051958
});

packages/plugins/eslint-plugin-react/src/rules/no-unstable-nested-components.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
} from "@eslint-react/ast";
1010
import {
1111
ERComponentCollectorHint,
12-
isInsideReactHookCall,
1312
isInsideRenderMethod,
1413
unsafeIsDeclaredInRenderProp,
1514
unsafeIsDirectValueOfRenderProperty,
@@ -73,12 +72,8 @@ export default createRule<[], MessageID>({
7372
return isClass(node) && classComponents.some(component => component.node === node);
7473
};
7574
for (const { node: component } of functionComponents) {
76-
if (
77-
// Do not mark components declared inside hooks (or falsy '() => null' clean-up methods)
78-
isInsideReactHookCall(component)
79-
// Do not mark objects containing render methods
80-
|| unsafeIsDirectValueOfRenderProperty(component)
81-
) {
75+
// Do not mark objects containing render methods
76+
if (unsafeIsDirectValueOfRenderProperty(component)) {
8277
continue;
8378
}
8479
const isInsideProperty = component.parent.type === NodeType.Property;

0 commit comments

Comments
 (0)