Skip to content

Commit 8865e3a

Browse files
authored
Add no-use-responsive-value rule (#443)
* Add no-use-responsive-value * Add changeset
1 parent 251b1e9 commit 8865e3a

File tree

6 files changed

+295
-0
lines changed

6 files changed

+295
-0
lines changed

.changeset/neat-mammals-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-primer-react': minor
3+
---
4+
5+
Add no-use-responsive-value
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# no-use-responsive-value
2+
3+
Disallow using `useResponsiveValue` hook from `@primer/react` or local imports.
4+
5+
## Rule Details
6+
7+
This rule prevents the use of the `useResponsiveValue` hook from:
8+
9+
- `@primer/react` package imports (including `/experimental` and `/deprecated` entrypoints)
10+
- Local file imports (relative paths containing `useResponsiveValue`)
11+
12+
### Why?
13+
14+
This hook is not fully SSR compatible as it relies on `useMediaUnsafeSSR` without a `defaultState`. Using `getResponsiveAttributes` is preferred to avoid hydration mismatches. This rule helps enforce consistent usage of SSR-safe responsive patterns across the codebase.
15+
16+
## Examples
17+
18+
### ❌ Incorrect
19+
20+
```js
21+
import {useResponsiveValue} from '@primer/react'
22+
23+
function Component() {
24+
const value = useResponsiveValue(['sm', 'md', 'lg'])
25+
return <div>{value}</div>
26+
}
27+
```
28+
29+
```js
30+
import {Button, useResponsiveValue} from '@primer/react'
31+
```
32+
33+
```js
34+
import {useResponsiveValue} from '@primer/react/experimental'
35+
```
36+
37+
```js
38+
import {useResponsiveValue} from '../hooks/useResponsiveValue'
39+
```
40+
41+
```js
42+
import useResponsiveValue from '../hooks/useResponsiveValue'
43+
```
44+
45+
```js
46+
import {useResponsiveValue} from './useResponsiveValue'
47+
```
48+
49+
### ✅ Correct
50+
51+
```js
52+
import {Button} from '@primer/react'
53+
54+
function Component() {
55+
// Use alternative responsive patterns
56+
return <Button>Click me</Button>
57+
}
58+
```
59+
60+
```js
61+
import {useResponsiveValue} from 'other-library'
62+
63+
function Component() {
64+
// Using useResponsiveValue from a different library is allowed
65+
const value = useResponsiveValue(['sm', 'md', 'lg'])
66+
return <div>{value}</div>
67+
}
68+
```
69+
70+
```js
71+
import {useCustomHook} from '../hooks/useCustomHook'
72+
73+
function Component() {
74+
// Importing other hooks from local paths is allowed
75+
const value = useCustomHook(['sm', 'md', 'lg'])
76+
return <div>{value}</div>
77+
}
78+
```
79+
80+
```js
81+
function useResponsiveValue() {
82+
// Local function definitions are allowed
83+
return 'custom implementation'
84+
}
85+
86+
function Component() {
87+
const value = useResponsiveValue()
88+
return <div>{value}</div>
89+
}
90+
```
91+
92+
## When Not To Use It
93+
94+
If your project needs to use `useResponsiveValue` from `@primer/react`, you can disable this rule:
95+
96+
```js
97+
/* eslint primer-react/no-use-responsive-value: "off" */
98+
```
99+
100+
## Options
101+
102+
This rule has no options.

src/configs/recommended.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module.exports = {
2323
'primer-react/a11y-use-accessible-tooltip': 'error',
2424
'primer-react/no-unnecessary-components': 'error',
2525
'primer-react/prefer-action-list-item-onselect': 'error',
26+
'primer-react/no-use-responsive-value': 'error',
2627
},
2728
settings: {
2829
github: {

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = {
2121
'enforce-css-module-default-import': require('./rules/enforce-css-module-default-import'),
2222
'use-styled-react-import': require('./rules/use-styled-react-import'),
2323
'spread-props-first': require('./rules/spread-props-first'),
24+
'no-use-responsive-value': require('./rules/no-use-responsive-value'),
2425
},
2526
configs: {
2627
recommended: require('./configs/recommended'),
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use strict'
2+
3+
const {RuleTester} = require('eslint')
4+
const rule = require('../no-use-responsive-value')
5+
6+
const ruleTester = new RuleTester({
7+
languageOptions: {
8+
ecmaVersion: 'latest',
9+
sourceType: 'module',
10+
parserOptions: {
11+
ecmaFeatures: {
12+
jsx: true,
13+
},
14+
},
15+
},
16+
})
17+
18+
ruleTester.run('no-use-responsive-value', rule, {
19+
valid: [
20+
// Valid - not importing useResponsiveValue
21+
`import { Button } from '@primer/react'`,
22+
23+
// Valid - importing from other modules
24+
`import { useResponsiveValue } from 'other-module'`,
25+
26+
// Valid - using other hooks from @primer/react
27+
`import { useTheme } from '@primer/react'`,
28+
29+
// Valid - function with same name but not imported from @primer/react
30+
`function useResponsiveValue() { return 'custom' }`,
31+
32+
// Valid - importing from unrelated local paths
33+
`import { something } from '../utils/helpers'`,
34+
35+
// Valid - importing other hooks from local paths
36+
`import { useCustomHook } from '../hooks/useCustomHook'`,
37+
],
38+
invalid: [
39+
// Invalid - importing useResponsiveValue from @primer/react
40+
{
41+
code: `import { useResponsiveValue } from '@primer/react'`,
42+
errors: [
43+
{
44+
messageId: 'noUseResponsiveValue',
45+
},
46+
],
47+
},
48+
49+
// Invalid - importing with other imports
50+
{
51+
code: `import { Button, useResponsiveValue, Box } from '@primer/react'`,
52+
errors: [
53+
{
54+
messageId: 'noUseResponsiveValue',
55+
},
56+
],
57+
},
58+
59+
// Invalid - importing as named import with alias
60+
{
61+
code: `import { useResponsiveValue as useRV } from '@primer/react'
62+
function Component() {
63+
const value = useRV(['sm', 'md'])
64+
return <div>{value}</div>
65+
}`,
66+
errors: [
67+
{
68+
messageId: 'noUseResponsiveValue',
69+
},
70+
],
71+
},
72+
73+
// Invalid - importing from experimental entrypoint
74+
{
75+
code: `import { useResponsiveValue } from '@primer/react/experimental'`,
76+
errors: [
77+
{
78+
messageId: 'noUseResponsiveValue',
79+
},
80+
],
81+
},
82+
83+
// Invalid - importing from deprecated entrypoint
84+
{
85+
code: `import { useResponsiveValue } from '@primer/react/deprecated'`,
86+
errors: [
87+
{
88+
messageId: 'noUseResponsiveValue',
89+
},
90+
],
91+
},
92+
93+
// Invalid - importing from local hooks path
94+
{
95+
code: `import { useResponsiveValue } from '../hooks/useResponsiveValue'`,
96+
errors: [
97+
{
98+
messageId: 'noUseResponsiveValue',
99+
},
100+
],
101+
},
102+
103+
// Invalid - importing default from local useResponsiveValue file
104+
{
105+
code: `import useResponsiveValue from '../hooks/useResponsiveValue'`,
106+
errors: [
107+
{
108+
messageId: 'noUseResponsiveValue',
109+
},
110+
],
111+
},
112+
113+
// Invalid - importing from nested path containing useResponsiveValue
114+
{
115+
code: `import { useResponsiveValue } from '../../src/hooks/useResponsiveValue'`,
116+
errors: [
117+
{
118+
messageId: 'noUseResponsiveValue',
119+
},
120+
],
121+
},
122+
123+
// Invalid - importing from lib path containing useResponsiveValue
124+
{
125+
code: `import { useResponsiveValue } from './useResponsiveValue'`,
126+
errors: [
127+
{
128+
messageId: 'noUseResponsiveValue',
129+
},
130+
],
131+
},
132+
],
133+
})
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict'
2+
3+
const url = require('../url')
4+
5+
/**
6+
* @type {import('eslint').Rule.RuleModule}
7+
*/
8+
module.exports = {
9+
meta: {
10+
type: 'problem',
11+
docs: {
12+
description: 'Disallow using useResponsiveValue hook',
13+
recommended: true,
14+
url: url(module),
15+
},
16+
schema: [],
17+
messages: {
18+
noUseResponsiveValue: 'useResponsiveValue is not allowed. Use alternative responsive patterns instead.',
19+
},
20+
},
21+
create(context) {
22+
return {
23+
// Check for import declarations
24+
ImportDeclaration(node) {
25+
// Check for @primer/react imports
26+
const isPrimerImport = /@primer\/react/.test(node.source.value)
27+
// Check for local imports that might be useResponsiveValue hook
28+
const isLocalUseResponsiveValueImport =
29+
node.source.value.includes('useResponsiveValue') || node.source.value.includes('/hooks/useResponsiveValue')
30+
31+
if (!isPrimerImport && !isLocalUseResponsiveValueImport) {
32+
return
33+
}
34+
35+
for (const specifier of node.specifiers) {
36+
if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'useResponsiveValue') {
37+
context.report({
38+
node: specifier,
39+
messageId: 'noUseResponsiveValue',
40+
})
41+
}
42+
// Also check for default imports from useResponsiveValue files
43+
if (specifier.type === 'ImportDefaultSpecifier' && isLocalUseResponsiveValueImport) {
44+
context.report({
45+
node: specifier,
46+
messageId: 'noUseResponsiveValue',
47+
})
48+
}
49+
}
50+
},
51+
}
52+
},
53+
}

0 commit comments

Comments
 (0)