Skip to content

Commit 3cd6465

Browse files
committed
feat(plugins/x): add 'no-use-context', closes #930
1 parent d42bb89 commit 3cd6465

File tree

9 files changed

+355
-0
lines changed

9 files changed

+355
-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: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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 { use, useContext } from 'react'
36+
37+
export const Component = () => {
38+
const value = useContext(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)
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 React from 'react'
63+
64+
export const Component = () => {
65+
const value = React.useContext(MyContext)
66+
return <div>{value}</div>
67+
}
68+
`,
69+
errors: [
70+
{ messageId: "noUseContext" },
71+
],
72+
output: /* tsx */ `
73+
import React from 'react'
74+
75+
export const Component = () => {
76+
const value = React.use(MyContext)
77+
return <div>{value}</div>
78+
}
79+
`,
80+
settings: {
81+
"react-x": {
82+
version: "19.0.0",
83+
},
84+
},
85+
},
86+
{
87+
code: /* tsx */ `
88+
import { use, useContext as useCtx } from 'react'
89+
90+
export const Component = () => {
91+
const value = useCtx(MyContext)
92+
return <div>{value}</div>
93+
}
94+
`,
95+
errors: [
96+
{ messageId: "noUseContext" },
97+
{ messageId: "noUseContext" },
98+
],
99+
output: /* tsx */ `
100+
import { use, useContext as useCtx } from 'react'
101+
102+
export const Component = () => {
103+
const value = use(MyContext)
104+
return <div>{value}</div>
105+
}
106+
`,
107+
settings: {
108+
"react-x": {
109+
version: "19.0.0",
110+
},
111+
},
112+
},
113+
],
114+
valid: [
115+
{
116+
code: /* tsx */ `
117+
import { useContext } from 'react'
118+
119+
export const Component = () => {
120+
const value = useContext(MyContext)
121+
return <div>{value}</div>
122+
}
123+
`,
124+
settings: {
125+
"react-x": {
126+
version: "18.3.1",
127+
},
128+
},
129+
},
130+
{
131+
code: /* tsx */ `
132+
import { use } from 'react'
133+
134+
export const Component = () => {
135+
const value = use(MyContext)
136+
return <div>{value}</div>
137+
}
138+
`,
139+
settings: {
140+
"react-x": {
141+
version: "19.0.0",
142+
},
143+
},
144+
},
145+
{
146+
code: /* tsx */ `
147+
import React from 'react'
148+
149+
export const Component = () => {
150+
const value = React.use(MyContext)
151+
return <div>{value}</div>
152+
}
153+
`,
154+
settings: {
155+
"react-x": {
156+
version: "19.0.0",
157+
},
158+
},
159+
},
160+
],
161+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
8+
import { createRule } from "../utils";
9+
10+
export const RULE_NAME = "no-use-context";
11+
12+
export const RULE_FEATURES = [
13+
"CHK",
14+
"MOD",
15+
] as const satisfies RuleFeature[];
16+
17+
export type MessageID = CamelCase<typeof RULE_NAME>;
18+
19+
export default createRule<[], MessageID>({
20+
meta: {
21+
type: "problem",
22+
docs: {
23+
description: "disallow the use of 'useContext'",
24+
[Symbol.for("rule_features")]: RULE_FEATURES,
25+
},
26+
fixable: "code",
27+
messages: {
28+
noUseContext: "In React 19, 'use' is preferred over 'useContext' because it is more flexible.",
29+
},
30+
schema: [],
31+
},
32+
name: RULE_NAME,
33+
create(context) {
34+
const settings = getSettingsFromContext(context);
35+
const useContextAlias = new Set<string>();
36+
37+
if (!context.sourceCode.text.includes("useContext")) {
38+
return {};
39+
}
40+
const { version } = getSettingsFromContext(context);
41+
if (compare(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+
let isUseImported = false;
71+
for (const specifier of node.specifiers) {
72+
if (specifier.type !== T.ImportSpecifier) continue;
73+
if (specifier.imported.type !== T.Identifier) continue;
74+
if (specifier.imported.name === "use") {
75+
isUseImported = true;
76+
}
77+
if (specifier.imported.name === "useContext") {
78+
if (specifier.local.name !== "useContext") {
79+
useContextAlias.add(specifier.local.name);
80+
context.report({
81+
messageId: "noUseContext",
82+
node: specifier,
83+
});
84+
return;
85+
}
86+
context.report({
87+
messageId: "noUseContext",
88+
node: specifier,
89+
fix(fixer) {
90+
if (isUseImported) {
91+
return fixer.replaceText(specifier, " ".repeat(specifier.range[1] - specifier.range[0]));
92+
}
93+
return fixer.replaceText(specifier.imported, "use");
94+
},
95+
});
96+
}
97+
}
98+
},
99+
};
100+
},
101+
defaultOptions: [],
102+
});

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",
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
title: no-use-context
3+
---
4+
5+
**Full Name in `eslint-plugin-react-x`**
6+
7+
```plain copy
8+
react-x/no-use-context
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```plain copy
14+
@eslint-react/no-use-context
15+
```
16+
17+
**Features**
18+
19+
`🔍` `🔄`
20+
21+
**Presets**
22+
23+
- `core`
24+
- `recommended`
25+
- `recommended-typescript`
26+
- `recommended-type-checked`
27+
28+
## What it does
29+
30+
Disallows using `React.useContext`.
31+
32+
In React 19, `use` is preferred over `useContext` because it is more flexible.
33+
34+
An **unsafe** codemod is available for this rule.
35+
36+
## Examples
37+
38+
### Failing
39+
40+
```tsx
41+
import { useContext } from "react";
42+
43+
const MyComponent = () => {
44+
const value = useContext(MyContext);
45+
return <div>{value}</div>;
46+
};
47+
```
48+
49+
```tsx
50+
import React from "react";
51+
52+
const MyComponent = () => {
53+
const value = React.useContext(MyContext);
54+
return <div>{value}</div>;
55+
};
56+
```
57+
58+
### Passing
59+
60+
```tsx
61+
import { use } from "react";
62+
63+
const MyComponent = () => {
64+
const value = use(MyContext);
65+
return <div>{value}</div>;
66+
};
67+
```
68+
69+
```tsx
70+
import React from "react";
71+
72+
const MyComponent = () => {
73+
const value = React.use(MyContext);
74+
return <div>{value}</div>;
75+
};
76+
```
77+
78+
## Implementation
79+
80+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-use-context.ts)
81+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-use-context.spec.ts)
82+
83+
## Further Reading
84+
85+
- [React: Reading context with use](https://react.dev/reference/react/use#reading-context-with-use)

0 commit comments

Comments
 (0)