Skip to content

Commit 0d60aac

Browse files
authored
feat(plugins/x): add 'no-use-context', closes #930 (#931)
1 parent d42bb89 commit 0d60aac

File tree

9 files changed

+363
-0
lines changed

9 files changed

+363
-0
lines changed

packages/plugins/eslint-plugin-react-x/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export default [
6565
"react-x/no-unstable-default-props": "warn",
6666
"react-x/no-unused-class-component-members": "warn",
6767
"react-x/no-unused-state": "warn",
68+
"react-x/no-use-context": "warn",
6869
"react-x/use-jsx-vars": "warn",
6970
},
7071
},

packages/plugins/eslint-plugin-react-x/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import noUnstableContextValue from "./rules/no-unstable-context-value";
4242
import noUnstableDefaultProps from "./rules/no-unstable-default-props";
4343
import noUnusedClassComponentMembers from "./rules/no-unused-class-component-members";
4444
import noUnusedState from "./rules/no-unused-state";
45+
import noUseContext from "./rules/no-use-context";
4546
import noUselessFragment from "./rules/no-useless-fragment";
4647
import preferDestructuringAssignment from "./rules/prefer-destructuring-assignment";
4748
import preferReactNamespaceImport from "./rules/prefer-react-namespace-import";
@@ -99,6 +100,7 @@ export default {
99100
"no-unstable-default-props": noUnstableDefaultProps,
100101
"no-unused-class-component-members": noUnusedClassComponentMembers,
101102
"no-unused-state": noUnusedState,
103+
"no-use-context": noUseContext,
102104
"no-useless-fragment": noUselessFragment,
103105
"prefer-destructuring-assignment": preferDestructuringAssignment,
104106
"prefer-react-namespace-import": preferReactNamespaceImport,
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { ruleTester } from "../../../../../test";
2+
import rule, { RULE_NAME } from "./no-use-context";
3+
4+
ruleTester.run(RULE_NAME, rule, {
5+
invalid: [
6+
{
7+
code: /* tsx */ `
8+
import { useContext } from 'react'
9+
10+
export const Component = () => {
11+
const value = useContext(MyContext)
12+
return <div>{value}</div>
13+
}
14+
`,
15+
errors: [
16+
{ messageId: "noUseContext" },
17+
{ messageId: "noUseContext" },
18+
],
19+
output: /* tsx */ `
20+
import { use } from 'react'
21+
22+
export const Component = () => {
23+
const value = use(MyContext)
24+
return <div>{value}</div>
25+
}
26+
`,
27+
settings: {
28+
"react-x": {
29+
version: "19.0.0",
30+
},
31+
},
32+
},
33+
{
34+
code: /* tsx */ `
35+
import { useContext } from 'react'
36+
37+
export const Component = () => {
38+
const value = useContext<MyContext>(MyContext)
39+
return <div>{value}</div>
40+
}
41+
`,
42+
errors: [
43+
{ messageId: "noUseContext" },
44+
{ messageId: "noUseContext" },
45+
],
46+
output: /* tsx */ `
47+
import { use } from 'react'
48+
49+
export const Component = () => {
50+
const value = use<MyContext>(MyContext)
51+
return <div>{value}</div>
52+
}
53+
`,
54+
settings: {
55+
"react-x": {
56+
version: "19.0.0",
57+
},
58+
},
59+
},
60+
{
61+
code: /* tsx */ `
62+
import { use, useContext } from 'react'
63+
64+
export const Component = () => {
65+
const value = useContext(MyContext)
66+
return <div>{value}</div>
67+
}
68+
`,
69+
errors: [
70+
{ messageId: "noUseContext" },
71+
{ messageId: "noUseContext" },
72+
],
73+
output: /* tsx */ `
74+
import { use, } from 'react'
75+
76+
export const Component = () => {
77+
const value = use(MyContext)
78+
return <div>{value}</div>
79+
}
80+
`,
81+
settings: {
82+
"react-x": {
83+
version: "19.0.0",
84+
},
85+
},
86+
},
87+
{
88+
code: /* tsx */ `
89+
import React from 'react'
90+
91+
export const Component = () => {
92+
const value = React.useContext(MyContext)
93+
return <div>{value}</div>
94+
}
95+
`,
96+
errors: [
97+
{ messageId: "noUseContext" },
98+
],
99+
output: /* tsx */ `
100+
import React from 'react'
101+
102+
export const Component = () => {
103+
const value = React.use(MyContext)
104+
return <div>{value}</div>
105+
}
106+
`,
107+
settings: {
108+
"react-x": {
109+
version: "19.0.0",
110+
},
111+
},
112+
},
113+
{
114+
code: /* tsx */ `
115+
import { use, useContext as useCtx } from 'react'
116+
117+
export const Component = () => {
118+
const value = useCtx(MyContext)
119+
return <div>{value}</div>
120+
}
121+
`,
122+
errors: [
123+
{ messageId: "noUseContext" },
124+
{ messageId: "noUseContext" },
125+
],
126+
output: /* tsx */ `
127+
import { use, useContext as useCtx } from 'react'
128+
129+
export const Component = () => {
130+
const value = use(MyContext)
131+
return <div>{value}</div>
132+
}
133+
`,
134+
settings: {
135+
"react-x": {
136+
version: "19.0.0",
137+
},
138+
},
139+
},
140+
],
141+
valid: [
142+
{
143+
code: /* tsx */ `
144+
import { useContext } from 'react'
145+
146+
export const Component = () => {
147+
const value = useContext(MyContext)
148+
return <div>{value}</div>
149+
}
150+
`,
151+
settings: {
152+
"react-x": {
153+
version: "18.3.1",
154+
},
155+
},
156+
},
157+
{
158+
code: /* tsx */ `
159+
import { use } from 'react'
160+
161+
export const Component = () => {
162+
const value = use(MyContext)
163+
return <div>{value}</div>
164+
}
165+
`,
166+
settings: {
167+
"react-x": {
168+
version: "19.0.0",
169+
},
170+
},
171+
},
172+
{
173+
code: /* tsx */ `
174+
import React from 'react'
175+
176+
export const Component = () => {
177+
const value = React.use(MyContext)
178+
return <div>{value}</div>
179+
}
180+
`,
181+
settings: {
182+
"react-x": {
183+
version: "19.0.0",
184+
},
185+
},
186+
},
187+
],
188+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { isReactHookCall, isReactHookCallWithNameAlias } from "@eslint-react/core";
2+
import type { RuleFeature } from "@eslint-react/shared";
3+
import { getSettingsFromContext } from "@eslint-react/shared";
4+
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
5+
import { compare } from "compare-versions";
6+
import type { CamelCase } from "string-ts";
7+
import { isMatching } from "ts-pattern";
8+
9+
import { createRule } from "../utils";
10+
11+
export const RULE_NAME = "no-use-context";
12+
13+
export const RULE_FEATURES = [
14+
"CHK",
15+
"MOD",
16+
] as const satisfies RuleFeature[];
17+
18+
export type MessageID = CamelCase<typeof RULE_NAME>;
19+
20+
export default createRule<[], MessageID>({
21+
meta: {
22+
type: "problem",
23+
docs: {
24+
description: "disallow the use of 'useContext'",
25+
[Symbol.for("rule_features")]: RULE_FEATURES,
26+
},
27+
fixable: "code",
28+
messages: {
29+
noUseContext: "In React 19, 'use' is preferred over 'useContext' because it is more flexible.",
30+
},
31+
schema: [],
32+
},
33+
name: RULE_NAME,
34+
create(context) {
35+
const settings = getSettingsFromContext(context);
36+
const useContextAlias = new Set<string>();
37+
38+
if (!context.sourceCode.text.includes("useContext")) {
39+
return {};
40+
}
41+
if (compare(settings.version, "19.0.0", "<")) {
42+
return {};
43+
}
44+
return {
45+
CallExpression(node) {
46+
if (!isReactHookCall(node)) {
47+
return;
48+
}
49+
if (!isReactHookCallWithNameAlias("useContext", context, [...useContextAlias])(node)) {
50+
return;
51+
}
52+
context.report({
53+
messageId: "noUseContext",
54+
node,
55+
fix(fixer) {
56+
switch (node.callee.type) {
57+
case T.Identifier:
58+
return fixer.replaceText(node.callee, "use");
59+
case T.MemberExpression:
60+
return fixer.replaceText(node.callee.property, "use");
61+
}
62+
return null;
63+
},
64+
});
65+
},
66+
ImportDeclaration(node) {
67+
if (node.source.value !== settings.importSource) {
68+
return;
69+
}
70+
const isUseImported = node.specifiers
71+
.some(isMatching({ local: { type: T.Identifier, name: "use" } }));
72+
for (const specifier of node.specifiers) {
73+
if (specifier.type !== T.ImportSpecifier) continue;
74+
if (specifier.imported.type !== T.Identifier) continue;
75+
if (specifier.imported.name === "useContext") {
76+
if (specifier.local.name !== "useContext") {
77+
useContextAlias.add(specifier.local.name);
78+
context.report({
79+
messageId: "noUseContext",
80+
node: specifier,
81+
});
82+
return;
83+
}
84+
context.report({
85+
messageId: "noUseContext",
86+
node: specifier,
87+
fix(fixer) {
88+
if (isUseImported) {
89+
return fixer.replaceText(specifier, " ".repeat(specifier.range[1] - specifier.range[0]));
90+
}
91+
return fixer.replaceText(specifier.imported, "use");
92+
},
93+
});
94+
}
95+
}
96+
},
97+
};
98+
},
99+
defaultOptions: [],
100+
});

packages/plugins/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const rules = {
5050
"@eslint-react/no-unstable-default-props": "warn",
5151
"@eslint-react/no-unused-class-component-members": "warn",
5252
"@eslint-react/no-unused-state": "warn",
53+
"@eslint-react/no-use-context": "warn",
5354
"@eslint-react/no-useless-fragment": "warn",
5455
"@eslint-react/prefer-destructuring-assignment": "warn",
5556
"@eslint-react/prefer-shorthand-boolean": "warn",

packages/plugins/eslint-plugin/src/configs/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const rules = {
4141
"@eslint-react/no-unstable-default-props": "warn",
4242
"@eslint-react/no-unused-class-component-members": "warn",
4343
"@eslint-react/no-unused-state": "warn",
44+
"@eslint-react/no-use-context": "warn",
4445
"@eslint-react/use-jsx-vars": "warn",
4546
} as const satisfies RulePreset;
4647

website/content/docs/rules/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"no-unstable-default-props",
4444
"no-unused-class-component-members",
4545
"no-unused-state",
46+
"no-use-context",
4647
"no-useless-fragment",
4748
"prefer-destructuring-assignment",
4849
"prefer-react-namespace-import",

0 commit comments

Comments
 (0)