Skip to content

Commit

Permalink
refactor: Improve rule react/no-unstable-nested-components, make it…
Browse files Browse the repository at this point in the history
…s behavior closer to facebook/react#25360 (#255)
  • Loading branch information
Rel1cx authored Dec 25, 2023
1 parent 38da7e5 commit 2b4db09
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 164 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 🪄 Improvements

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

## v0.10.0 (Thu Dec 21 2023)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,63 +47,6 @@ ruleTester.run(RULE_NAME, rule, {
});
}
`,
dedent`
function ParentComponent() {
const MemoizedNestedComponent = React.useCallback(() => <div />, []);
return (
<div>
<MemoizedNestedComponent />
</div>
);
}
`,
dedent`
function ParentComponent() {
const MemoizedNestedComponent = React.useCallback(
() => React.createElement("div", null),
[]
);
return React.createElement(
"div",
null,
React.createElement(MemoizedNestedComponent, null)
);
}
`,
dedent`
function ParentComponent() {
const MemoizedNestedFunctionComponent = React.useCallback(
function () {
return <div />;
},
[]
);
return (
<div>
<MemoizedNestedFunctionComponent />
</div>
);
}
`,
dedent`
function ParentComponent() {
const MemoizedNestedFunctionComponent = React.useCallback(
function () {
return React.createElement("div", null);
},
[]
);
return React.createElement(
"div",
null,
React.createElement(MemoizedNestedFunctionComponent, null)
);
}
`,
dedent`
function ParentComponent(props) {
// Should not interfere handler declarations
Expand Down Expand Up @@ -402,69 +345,6 @@ ruleTester.run(RULE_NAME, rule, {
}
}
`,
dedent`
function App({ locale }: AppProps) {
const route = Router.useRoute(["Home", "BotArea", "NotFound"]);
return (
<TypesafeI18n locale={locale}>
<MantineProvider theme={mantineTheme}>
<div className={css.root}>
<React.Suspense fallback={<RootLayout navHeader={<small className={css.loading} />} />}>
{React.useMemo(
() => match(route)
.with({ name: "Home" }, () => <Redirect to="/bots/ChatGPT" />)
.with({ name: "BotArea" }, ({ params }) => <BotArea botName={params.botName} />)
.otherwise(() => <NotFound />),
[loaded, route],
)}
</React.Suspense>
</div>
</MantineProvider>
</TypesafeI18n>
);
}
`,
dedent`
function BotArea({ botName }: BotAreaProps) {
const bot = useAtomValue(botsDb.item(botName));
const route = Router.useRoute(["BotRoot", "BotChat", "BotNewChat", "BotSettings"]);
const botList = useAtomValue(botListAtom);
const contentView = React.useMemo(
() =>
match(route)
.with({ name: "BotRoot" }, ({ params }) => <RedirectChat botName={params.botName} />)
.with({ name: "BotNewChat" }, ({ params }) => <RedirectChat botName={params.botName} />)
.with({ name: "BotSettings" }, ({ params }) => <BotSettings botName={params.botName} />)
.with({ name: "BotChat" }, ({ params }) => {
const { botName, chatID } = params;
if (!ID.isChatID(chatID)) {
return <Redirect to="/404" />;
}
return <ChatDetail botName={botName} chatID={chatID} />;
})
.otherwise(() => null),
[route],
);
if (!bot) {
return <Redirect to="/404" />;
}
return (
<BotProvider botName={botName}>
<RootLayout nav={<BotList items={botList} selected={botName} />}>
<ErrorBoundary fallback={<p className="p-2">Failed to render bot area.</p>}>
<React.Suspense>{contentView}</React.Suspense>
</ErrorBoundary>
</RootLayout>
</BotProvider>
);
}
`,
dedent`
function ComponentWithProps(props) {
return <div />;
Expand Down Expand Up @@ -1007,45 +887,72 @@ ruleTester.run(RULE_NAME, rule, {
},
{
code: dedent`
export function BotList({ items, selected }: BotListProps) {
return (
<div className={css.root}>
{items.map((item) => {
return match(item)
.when(
({ id }) => id === selected,
({ id, title, icon }) => (
<BotMenu botName={title} key={id}>
<Button
aria-label="bot-button"
render={<button type="button" />}
clickOnEnter
clickOnSpace
>
<Avatar bg={icon} />
</Button>
</BotMenu>
),
)
.otherwise(({ id, title, icon }) => (
<Link key={id} to={\`/bots/\${title}\`}>
<Avatar bg={icon} />
</Link>
));
})}
<Indicator label="WIP" size={14} inline>
<div className={css.plus}>
<Icon as={Plus} color={vars.colors.overlay} size={24} />
</div>
</Indicator>
</div>
);
function ParentComponent() {
const MemoizedNestedComponent = React.useCallback(() => <div />, []);
return (
<div>
<MemoizedNestedComponent />
</div>
);
}
`,
errors: [
{ messageId: "UNSTABLE_NESTED_COMPONENT" },
{ messageId: "UNSTABLE_NESTED_COMPONENT" },
],
errors: [{ messageId: "UNSTABLE_NESTED_COMPONENT" }],
},
{
code: dedent`
function ParentComponent() {
const MemoizedNestedComponent = React.useCallback(
() => React.createElement("div", null),
[]
);
return React.createElement(
"div",
null,
React.createElement(MemoizedNestedComponent, null)
);
}
`,
errors: [{ messageId: "UNSTABLE_NESTED_COMPONENT" }],
},
{
code: dedent`
function ParentComponent() {
const MemoizedNestedFunctionComponent = React.useCallback(
function () {
return <div />;
},
[]
);
return (
<div>
<MemoizedNestedFunctionComponent />
</div>
);
}
`,
errors: [{ messageId: "UNSTABLE_NESTED_COMPONENT" }],
},
{
code: dedent`
function ParentComponent() {
const MemoizedNestedFunctionComponent = React.useCallback(
function () {
return React.createElement("div", null);
},
[]
);
return React.createElement(
"div",
null,
React.createElement(MemoizedNestedFunctionComponent, null)
);
}
`,
errors: [{ messageId: "UNSTABLE_NESTED_COMPONENT" }],
},
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from "@eslint-react/ast";
import {
ERComponentCollectorHint,
isInsideReactHookCall,
isInsideRenderMethod,
unsafeIsDeclaredInRenderProp,
unsafeIsDirectValueOfRenderProperty,
Expand Down Expand Up @@ -73,12 +72,8 @@ export default createRule<[], MessageID>({
return isClass(node) && classComponents.some(component => component.node === node);
};
for (const { node: component } of functionComponents) {
if (
// Do not mark components declared inside hooks (or falsy '() => null' clean-up methods)
isInsideReactHookCall(component)
// Do not mark objects containing render methods
|| unsafeIsDirectValueOfRenderProperty(component)
) {
// Do not mark objects containing render methods
if (unsafeIsDirectValueOfRenderProperty(component)) {
continue;
}
const isInsideProperty = component.parent.type === NodeType.Property;
Expand Down

0 comments on commit 2b4db09

Please sign in to comment.