@ashstack/lint 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +1 -1
  2. package/dist/core/index.js +1 -222
  3. package/dist/core/rules/core/components-tsx-only.js +1 -37
  4. package/dist/core/rules/core/hoist-intl.js +1 -32
  5. package/dist/core/rules/core/index.js +1 -18
  6. package/dist/core/rules/core/no-comments.js +1 -194
  7. package/dist/core/rules/core/no-naming-convention.js +1 -143
  8. package/dist/core/rules/core/use-design-system.js +1 -147
  9. package/dist/core/rules/zod/index.js +1 -12
  10. package/dist/core/rules/zod/prefer-enum.js +1 -44
  11. package/dist/index.js +1 -3
  12. package/dist/lib/ast.js +1 -162
  13. package/dist/lib/detect.js +1 -42
  14. package/dist/lib/merge.js +1 -18
  15. package/dist/lib/module.js +1 -49
  16. package/dist/lib/types.js +1 -1
  17. package/dist/react/index.js +1 -126
  18. package/dist/react/rules/i18n/index.js +1 -16
  19. package/dist/react/rules/i18n/no-bare-attrs.js +1 -38
  20. package/dist/react/rules/i18n/no-bare-text.js +1 -26
  21. package/dist/react/rules/i18n/no-bare-toast.js +1 -38
  22. package/dist/react/rules/query/index.js +1 -20
  23. package/dist/react/rules/query/next-page-param-undefined.js +1 -26
  24. package/dist/react/rules/query/no-deprecated-filters.js +1 -63
  25. package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -22
  26. package/dist/react/rules/query/no-inline-keys.js +1 -107
  27. package/dist/react/rules/query/require-destructured-hooks.js +1 -41
  28. package/dist/react/rules/query/shared.js +1 -10
  29. package/dist/react/rules/zustand/index.js +1 -12
  30. package/dist/react/rules/zustand/require-selector.js +1 -62
  31. package/dist/react-native/index.js +1 -113
  32. package/dist/react-native/rules/keyboard/index.js +1 -18
  33. package/dist/react-native/rules/legend-list/index.js +1 -51
  34. package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -77
  35. package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -26
  36. package/dist/react-native/rules/legend-list/no-inline-data.js +1 -25
  37. package/dist/react-native/rules/legend-list/no-inline-extra-data.js +1 -21
  38. package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -35
  39. package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -24
  40. package/dist/react-native/rules/legend-list/no-remount-key.js +1 -18
  41. package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -27
  42. package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -29
  43. package/dist/react-native/rules/legend-list/required-props.js +1 -23
  44. package/dist/react-native/rules/legend-list/shared.js +1 -25
  45. package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -49
  46. package/dist/react-native/rules/legend-state/index.js +1 -35
  47. package/dist/react-native/rules/legend-state/naming.js +1 -26
  48. package/dist/react-native/rules/legend-state/no-assignment.js +1 -31
  49. package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -23
  50. package/dist/react-native/rules/legend-state/no-object-selector.js +1 -47
  51. package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -46
  52. package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -67
  53. package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -33
  54. package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -52
  55. package/dist/react-native/rules/legend-state/shared.js +1 -23
  56. package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -59
  57. package/dist/react-native/rules/react-native/index.js +1 -50
  58. package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -23
  59. package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -18
  60. package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -18
  61. package/dist/react-native/rules/react-native/no-leaked-render.js +1 -43
  62. package/dist/react-native/rules/react-native/no-manual-memo.js +1 -87
  63. package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -36
  64. package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -58
  65. package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -38
  66. package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -26
  67. package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -62
  68. package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -110
  69. package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -72
  70. package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -38
  71. package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -80
  72. package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +1 -43
  73. package/dist/react-native/rules/reanimated/index.js +1 -51
  74. package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -41
  75. package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -86
  76. package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +1 -112
  77. package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -24
  78. package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -21
  79. package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -105
  80. package/dist/react-native/rules/reanimated/shared.js +1 -13
  81. package/dist/react-native/rules/skia/canvas-opaque.js +1 -49
  82. package/dist/react-native/rules/skia/index.js +1 -21
  83. package/dist/react-native/rules/turbo-image/index.js +1 -14
  84. package/dist/react-native/rules/turbo-image/require-cache-policy.js +1 -21
  85. package/dist/react-native/rules/turbo-image/require-resize.js +1 -22
  86. package/dist/react-native/rules/unistyles/animated-theme.js +1 -37
  87. package/dist/react-native/rules/unistyles/content-container.js +1 -73
  88. package/dist/react-native/rules/unistyles/in-sheet.js +1 -116
  89. package/dist/react-native/rules/unistyles/index.js +1 -58
  90. package/dist/react-native/rules/unistyles/insets.js +1 -92
  91. package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -30
  92. package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -78
  93. package/dist/react-native/rules/unistyles/no-margin.js +1 -19
  94. package/dist/react-native/rules/unistyles/no-style-spread.js +1 -41
  95. package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -99
  96. package/dist/react-native/rules/unistyles/rtl-style-call.js +1 -42
  97. package/dist/react-native/rules/unistyles/shared.js +1 -70
  98. package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -22
  99. package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -18
  100. package/dist/react-native/stylesheet.js +1 -44
  101. package/package.json +3 -4
  102. package/RULES.md +0 -2881
package/RULES.md DELETED
@@ -1,2881 +0,0 @@
1
- <!-- GENERATED by scripts/generate-rule-docs.ts - do not edit by hand. `bun run docs:rules` regenerates it. -->
2
-
3
- # @ashstack/lint rules
4
-
5
- Find a rule by the id in its diagnostic, e.g. `@ashstack/unistyles/no-margin`. Each one lists what it enforces, its options, and a failing and a passing example. The diagnostic itself names the fix.
6
-
7
- `core()`, `react()` and `react-native()` each contain the one before, so a section's built-in table lists only the settings that entry changes. Built-in rules link to their upstream page.
8
-
9
- Turn any rule off by id in your own `rules` block: `"@ashstack/unistyles/no-margin": "off"`.
10
-
11
- - [`core()`](#core)
12
- - [`@ashstack/core`](#ashstackcore) — 5 rules
13
- - [`@ashstack/zod`](#ashstackzod) — 1 rule
14
- - [`react()`](#react)
15
- - [`@ashstack/query`](#ashstackquery) — 5 rules
16
- - [`@ashstack/zustand`](#ashstackzustand) — 1 rule
17
- - [`@ashstack/i18n`](#ashstacki18n) — 3 rules
18
- - [`react-native()`](#react-native)
19
- - [`@ashstack/react-native`](#ashstackreact-native) — 11 rules
20
- - [`@ashstack/unistyles`](#ashstackunistyles) — 12 rules
21
- - [`@ashstack/legend-list`](#ashstacklegend-list) — 11 rules
22
- - [`@ashstack/legend-state`](#ashstacklegend-state) — 8 rules
23
- - [`@ashstack/reanimated`](#ashstackreanimated) — 11 rules
24
- - [`@ashstack/turbo-image`](#ashstackturbo-image) — 2 rules
25
- - [`@ashstack/skia`](#ashstackskia) — 1 rule
26
- - [`@ashstack/keyboard`](#ashstackkeyboard) — import bans only
27
-
28
- ## `core()`
29
-
30
- Any TypeScript project — backend, CLI, library.
31
-
32
- Plugins: `eslint`, `typescript`, `import`, `unicorn`, `promise`, `oxc`.
33
-
34
- ### Built-in rules
35
-
36
- | Rule | Setting |
37
- | --- | --- |
38
- | [`array-callback-return`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/array-callback-return.html) | `"error"` |
39
- | [`constructor-super`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/constructor-super.html) | `"error"` |
40
- | [`default-param-last`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/default-param-last.html) | `"error"` |
41
- | [`eqeqeq`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/eqeqeq.html) | `["error","always",{"null":"ignore"}]` |
42
- | [`for-direction`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/for-direction.html) | `"error"` |
43
- | [`getter-return`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/getter-return.html) | `"error"` |
44
- | [`func-style`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/func-style.html) | `["error","expression"]` |
45
- | [`no-array-constructor`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-array-constructor.html) | `"error"` |
46
- | [`no-async-promise-executor`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-async-promise-executor.html) | `"error"` |
47
- | [`no-case-declarations`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-case-declarations.html) | `"error"` |
48
- | [`no-class-assign`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-class-assign.html) | `"error"` |
49
- | [`no-compare-neg-zero`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-compare-neg-zero.html) | `"error"` |
50
- | [`no-cond-assign`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-cond-assign.html) | `"error"` |
51
- | [`no-const-assign`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-const-assign.html) | `"error"` |
52
- | [`no-constant-condition`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-constant-condition.html) | `"error"` |
53
- | [`no-control-regex`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-control-regex.html) | `"error"` |
54
- | [`no-debugger`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html) | `"error"` |
55
- | [`no-dupe-class-members`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-dupe-class-members.html) | `"error"` |
56
- | [`no-dupe-else-if`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-dupe-else-if.html) | `"error"` |
57
- | [`no-dupe-keys`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-dupe-keys.html) | `"error"` |
58
- | [`no-duplicate-case`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-duplicate-case.html) | `"error"` |
59
- | [`no-empty`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-empty.html) | `"error"` |
60
- | [`no-empty-character-class`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-empty-character-class.html) | `"error"` |
61
- | [`no-empty-pattern`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-empty-pattern.html) | `"error"` |
62
- | [`no-ex-assign`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-ex-assign.html) | `"error"` |
63
- | [`no-extra-boolean-cast`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-extra-boolean-cast.html) | `"error"` |
64
- | [`no-fallthrough`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-fallthrough.html) | `"error"` |
65
- | [`no-func-assign`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-func-assign.html) | `"error"` |
66
- | [`no-global-assign`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-global-assign.html) | `"error"` |
67
- | [`no-import-assign`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-import-assign.html) | `"error"` |
68
- | [`no-irregular-whitespace`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-irregular-whitespace.html) | `"error"` |
69
- | [`no-loss-of-precision`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-loss-of-precision.html) | `"error"` |
70
- | [`no-misleading-character-class`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-misleading-character-class.html) | `"error"` |
71
- | [`no-negated-condition`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-negated-condition.html) | `"error"` |
72
- | [`no-nested-ternary`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-nested-ternary.html) | `"error"` |
73
- | [`no-new-native-nonconstructor`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-new-native-nonconstructor.html) | `"error"` |
74
- | [`no-nonoctal-decimal-escape`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-nonoctal-decimal-escape.html) | `"error"` |
75
- | [`no-obj-calls`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-obj-calls.html) | `"error"` |
76
- | [`no-param-reassign`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-param-reassign.html) | `"error"` |
77
- | [`no-prototype-builtins`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-prototype-builtins.html) | `"error"` |
78
- | [`no-redeclare`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-redeclare.html) | `"off"` |
79
- | [`no-regex-spaces`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-regex-spaces.html) | `"error"` |
80
- | [`no-self-assign`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-self-assign.html) | `"error"` |
81
- | [`no-setter-return`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-setter-return.html) | `"error"` |
82
- | [`no-shadow-restricted-names`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-shadow-restricted-names.html) | `"error"` |
83
- | [`no-sparse-arrays`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-sparse-arrays.html) | `"error"` |
84
- | [`no-this-before-super`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-this-before-super.html) | `"error"` |
85
- | [`no-undef`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-undef.html) | `"off"` |
86
- | [`no-unreachable`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unreachable.html) | `"error"` |
87
- | [`no-unsafe-finally`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unsafe-finally.html) | `"error"` |
88
- | [`no-unsafe-negation`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unsafe-negation.html) | `"error"` |
89
- | [`no-unsafe-optional-chaining`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unsafe-optional-chaining.html) | `"error"` |
90
- | [`no-unused-labels`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unused-labels.html) | `"error"` |
91
- | [`no-unused-private-class-members`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unused-private-class-members.html) | `"error"` |
92
- | [`no-useless-catch`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-useless-catch.html) | `"error"` |
93
- | [`no-useless-constructor`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-useless-constructor.html) | `"error"` |
94
- | [`no-useless-escape`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-useless-escape.html) | `"error"` |
95
- | [`no-var`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-var.html) | `"error"` |
96
- | [`no-with`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-with.html) | `"error"` |
97
- | [`object-shorthand`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/object-shorthand.html) | `["error","methods"]` |
98
- | [`prefer-rest-params`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/prefer-rest-params.html) | `"error"` |
99
- | [`require-yield`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/require-yield.html) | `"error"` |
100
- | [`use-isnan`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/use-isnan.html) | `"error"` |
101
- | [`valid-typeof`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/valid-typeof.html) | `"error"` |
102
- | [`no-unused-vars`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unused-vars.html) | `["error",{"args":"after-used","argsIgnorePattern":"^_","caughtErrors":"all","caughtErrorsIgnorePattern":"^_","varsIgnorePattern":"^_","destructuredArrayIgnorePattern":"^_","ignoreRestSiblings":true,"fix":{"imports":"safe-fix"}}]` |
103
- | [`no-use-before-define`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-use-before-define.html) | `"off"` |
104
- | [`complexity`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/complexity.html) | `["error",{"max":12}]` |
105
- | [`max-depth`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/max-depth.html) | `["error",3]` |
106
- | [`max-params`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/max-params.html) | `["error",4]` |
107
- | [`max-lines`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/max-lines.html) | `["error",{"max":300,"skipBlankLines":true,"skipComments":true}]` |
108
- | [`max-lines-per-function`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/max-lines-per-function.html) | `["error",{"max":250,"skipBlankLines":true,"skipComments":true}]` |
109
- | [`eslint/no-await-in-loop`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-await-in-loop.html) | `"error"` |
110
- | [`eslint/no-console`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-console.html) | `"off"` |
111
- | [`eslint/no-constant-binary-expression`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-constant-binary-expression.html) | `"off"` |
112
- | [`eslint/no-control-regex`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-control-regex.html) | `"error"` |
113
- | [`eslint/no-else-return`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-else-return.html) | `"error"` |
114
- | [`eslint/no-empty`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-empty.html) | `"error"` |
115
- | [`eslint/no-empty-function`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-empty-function.html) | `"error"` |
116
- | [`eslint/no-new`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-new.html) | `"off"` |
117
- | [`eslint/no-self-compare`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-self-compare.html) | `"error"` |
118
- | [`eslint/no-shadow`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-shadow.html) | `"error"` |
119
- | [`eslint/no-template-curly-in-string`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-template-curly-in-string.html) | `"error"` |
120
- | [`eslint/no-unassigned-vars`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unassigned-vars.html) | `"off"` |
121
- | [`eslint/no-underscore-dangle`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-underscore-dangle.html) | `"off"` |
122
- | [`eslint/no-unmodified-loop-condition`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unmodified-loop-condition.html) | `"off"` |
123
- | [`eslint/no-unused-expressions`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unused-expressions.html) | `"off"` |
124
- | [`eslint/no-useless-concat`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-useless-concat.html) | `"off"` |
125
- | [`eslint/preserve-caught-error`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/preserve-caught-error.html) | `"off"` |
126
- | [`eslint/prefer-arrow-callback`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/prefer-arrow-callback.html) | `"error"` |
127
- | [`eslint/prefer-const`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/prefer-const.html) | `"error"` |
128
- | [`eslint/prefer-template`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/prefer-template.html) | `"error"` |
129
- | [`typescript/consistent-type-imports`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/consistent-type-imports.html) | `"error"` |
130
- | [`typescript/dot-notation`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/dot-notation.html) | `"error"` |
131
- | [`typescript/no-confusing-void-expression`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-confusing-void-expression.html) | `"error"` |
132
- | [`typescript/no-empty-object-type`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-empty-object-type.html) | `["error",{"allowInterfaces":"with-single-extends"}]` |
133
- | [`typescript/no-explicit-any`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-explicit-any.html) | `"error"` |
134
- | [`typescript/no-extra-non-null-assertion`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-extra-non-null-assertion.html) | `"error"` |
135
- | [`typescript/no-extraneous-class`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-extraneous-class.html) | `"error"` |
136
- | [`typescript/no-floating-promises`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-floating-promises.html) | `"error"` |
137
- | [`typescript/no-import-type-side-effects`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-import-type-side-effects.html) | `"error"` |
138
- | [`typescript/no-misused-new`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-misused-new.html) | `"error"` |
139
- | [`typescript/no-misused-promises`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-misused-promises.html) | `["error",{"checksVoidReturn":false}]` |
140
- | [`typescript/no-namespace`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-namespace.html) | `"error"` |
141
- | [`typescript/no-non-null-asserted-optional-chain`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-non-null-asserted-optional-chain.html) | `"error"` |
142
- | [`typescript/no-non-null-assertion`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-non-null-assertion.html) | `"error"` |
143
- | [`typescript/no-unnecessary-boolean-literal-compare`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-unnecessary-boolean-literal-compare.html) | `"error"` |
144
- | [`typescript/no-unnecessary-condition`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-unnecessary-condition.html) | `"error"` |
145
- | [`typescript/no-unnecessary-type-assertion`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-unnecessary-type-assertion.html) | `"error"` |
146
- | [`typescript/no-unnecessary-type-constraint`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-unnecessary-type-constraint.html) | `"error"` |
147
- | [`typescript/no-unsafe-declaration-merging`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-unsafe-declaration-merging.html) | `"error"` |
148
- | [`typescript/no-unsafe-function-type`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-unsafe-function-type.html) | `"error"` |
149
- | [`typescript/no-unsafe-type-assertion`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-unsafe-type-assertion.html) | `"error"` |
150
- | [`typescript/no-wrapper-object-types`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-wrapper-object-types.html) | `"error"` |
151
- | [`typescript/prefer-as-const`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/prefer-as-const.html) | `"error"` |
152
- | [`typescript/prefer-namespace-keyword`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/prefer-namespace-keyword.html) | `"error"` |
153
- | [`typescript/prefer-nullish-coalescing`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/prefer-nullish-coalescing.html) | `"error"` |
154
- | [`typescript/require-await`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/require-await.html) | `"error"` |
155
- | [`typescript/switch-exhaustiveness-check`](https://oxc.rs/docs/guide/usage/linter/rules/typescript/switch-exhaustiveness-check.html) | `["error",{"considerDefaultExhaustiveForUnions":true}]` |
156
- | [`import/no-commonjs`](https://oxc.rs/docs/guide/usage/linter/rules/import/no-commonjs.html) | `"off"` |
157
- | [`import/no-cycle`](https://oxc.rs/docs/guide/usage/linter/rules/import/no-cycle.html) | `"error"` |
158
- | [`import/no-duplicates`](https://oxc.rs/docs/guide/usage/linter/rules/import/no-duplicates.html) | `"error"` |
159
- | [`import/no-named-as-default`](https://oxc.rs/docs/guide/usage/linter/rules/import/no-named-as-default.html) | `"off"` |
160
- | [`import/no-named-as-default-member`](https://oxc.rs/docs/guide/usage/linter/rules/import/no-named-as-default-member.html) | `"off"` |
161
- | [`import/no-self-import`](https://oxc.rs/docs/guide/usage/linter/rules/import/no-self-import.html) | `"off"` |
162
- | [`import/no-unassigned-import`](https://oxc.rs/docs/guide/usage/linter/rules/import/no-unassigned-import.html) | `"off"` |
163
- | [`unicorn/consistent-function-scoping`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/consistent-function-scoping.html) | `"off"` |
164
- | [`unicorn/no-array-reverse`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/no-array-reverse.html) | `"off"` |
165
- | [`unicorn/no-array-sort`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/no-array-sort.html) | `"off"` |
166
- | [`unicorn/no-empty-file`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/no-empty-file.html) | `"off"` |
167
- | [`unicorn/no-new-array`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/no-new-array.html) | `"off"` |
168
- | [`unicorn/no-useless-fallback-in-spread`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/no-useless-fallback-in-spread.html) | `"off"` |
169
- | [`unicorn/no-useless-spread`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/no-useless-spread.html) | `"off"` |
170
- | [`unicorn/prefer-add-event-listener`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/prefer-add-event-listener.html) | `"off"` |
171
- | [`unicorn/prefer-array-flat-map`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/prefer-array-flat-map.html) | `"error"` |
172
- | [`unicorn/prefer-date-now`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/prefer-date-now.html) | `"error"` |
173
- | [`unicorn/prefer-logical-operator-over-ternary`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/prefer-logical-operator-over-ternary.html) | `"error"` |
174
- | [`unicorn/prefer-node-protocol`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/prefer-node-protocol.html) | `"error"` |
175
- | [`unicorn/prefer-optional-catch-binding`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/prefer-optional-catch-binding.html) | `"error"` |
176
- | [`unicorn/require-module-specifiers`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/require-module-specifiers.html) | `"off"` |
177
- | [`unicorn/throw-new-error`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/throw-new-error.html) | `"error"` |
178
- | [`promise/always-return`](https://oxc.rs/docs/guide/usage/linter/rules/promise/always-return.html) | `"off"` |
179
- | [`promise/no-callback-in-promise`](https://oxc.rs/docs/guide/usage/linter/rules/promise/no-callback-in-promise.html) | `"off"` |
180
- | [`promise/no-multiple-resolved`](https://oxc.rs/docs/guide/usage/linter/rules/promise/no-multiple-resolved.html) | `"off"` |
181
- | [`promise/no-promise-in-callback`](https://oxc.rs/docs/guide/usage/linter/rules/promise/no-promise-in-callback.html) | `"off"` |
182
- | [`oxc/no-async-endpoint-handlers`](https://oxc.rs/docs/guide/usage/linter/rules/oxc/no-async-endpoint-handlers.html) | `"off"` |
183
-
184
- ### `@ashstack/core`
185
-
186
- _always on via `core()` and every entry above it (opt-in rules noted per rule)._
187
-
188
- #### `@ashstack/core/no-comments`
189
-
190
- Disallow every comment that is neither a `// what: <fact>` line nor a tooling directive. The message names the refactoring that removes it: Rename, Extract Function, Guard Clause. Surviving `// what:` lines are held to one short line each, at most `budget` per file (default 2); `escapeHatch: false` removes the hatch entirely, so no prose survives at all. With `jsdoc: "allow"`, a `/** */` block documenting the declaration directly beneath it is kept, while a floating one still reports.
191
-
192
- > Off by default — opt in per project.
193
-
194
- **Options**
195
-
196
- ```jsonc
197
- [
198
- {
199
- "type": "object",
200
- "properties": {
201
- "jsdoc": {
202
- "enum": [
203
- "allow",
204
- "report"
205
- ]
206
- },
207
- "escapeHatch": {
208
- "type": "boolean"
209
- },
210
- "budget": {
211
- "type": "integer",
212
- "minimum": 0
213
- }
214
- },
215
- "additionalProperties": false
216
- }
217
- ]
218
- ```
219
-
220
- **Fails**
221
-
222
- ```tsx
223
- import { View } from "react-native";
224
-
225
- // this panel is the one the settings screen renders
226
- export const Panel = () => {
227
- /** the flag that decides whether the footer shows */
228
- const flag = true;
229
- // what: short
230
- // what: iOS 17 reports the wrong inset for the first layout pass here.
231
- // what: Android fires this layout pass twice below API 31 on Samsung devices.
232
- // what: The upstream contract sends the cursor as a string, never a number.
233
- return <View>{flag}</View>;
234
- };
235
- ```
236
-
237
- **Passes**
238
-
239
- ```tsx
240
- import { View } from "react-native";
241
-
242
- /** Renders the settings screen's panel. */
243
- export const SettingsPanel = () => {
244
- // what: Android fires this layout pass twice below API 31.
245
- const showsFooter = true;
246
-
247
- // what: The upstream contract sends the cursor as a string, never a number.
248
- return <View>{showsFooter}</View>;
249
- };
250
- ```
251
-
252
- #### `@ashstack/core/no-naming-convention`
253
-
254
- Require a variable, object property, type member or enum member to use one of the casings allowed for its kind.
255
-
256
- **Fails**
257
-
258
- ```tsx
259
- import { View } from "react-native";
260
-
261
- export const bad_Name = 1;
262
-
263
- export const styles = {
264
- mixed_Snake_Case: 1,
265
- };
266
-
267
- export const Panel = () => <View />;
268
- ```
269
-
270
- **Passes**
271
-
272
- ```tsx
273
- import { View } from "react-native";
274
-
275
- export const camelVariable = 1;
276
- export const CONSTANT_VARIABLE = 2;
277
- export const unstable_settings = { initialRouteName: "index" };
278
- export const settings$ = { theme$: "dark" };
279
-
280
- export const styles = {
281
- camelCase: 1,
282
- snake_case: 2,
283
- CONSTANT_CASE: 3,
284
- PascalCase: 4,
285
- experimental_backgroundImage: 5,
286
- unstable_conditionNames: 6,
287
- enableFullScreenImage_legacy: 7,
288
- };
289
-
290
- export const Panel = () => <View />;
291
- ```
292
-
293
- #### `@ashstack/core/use-design-system`
294
-
295
- Disallow importing a raw primitive your design system already wraps. Wrapped components come from scanning the design-system directory, plus the explicit `use` map for names, paths and source modules the scan cannot infer. Files under the design-system directory (or `exempt`) are skipped.
296
-
297
- > Off by default — opt in per project.
298
-
299
- **Options**
300
-
301
- ```jsonc
302
- [
303
- {
304
- "type": "object",
305
- "properties": {
306
- "dir": {
307
- "type": "string",
308
- "minLength": 1
309
- },
310
- "alias": {
311
- "type": "string",
312
- "minLength": 1
313
- },
314
- "use": {
315
- "type": "object",
316
- "additionalProperties": {
317
- "anyOf": [
318
- {
319
- "type": "string"
320
- },
321
- {
322
- "type": "array",
323
- "items": {
324
- "type": "string"
325
- }
326
- },
327
- {
328
- "type": "object",
329
- "properties": {
330
- "replaces": {
331
- "anyOf": [
332
- {
333
- "type": "string"
334
- },
335
- {
336
- "type": "array",
337
- "items": {
338
- "type": "string"
339
- }
340
- }
341
- ]
342
- },
343
- "from": {
344
- "type": "string"
345
- },
346
- "path": {
347
- "type": "string"
348
- },
349
- "reason": {
350
- "type": "string"
351
- }
352
- },
353
- "required": [
354
- "replaces"
355
- ],
356
- "additionalProperties": false
357
- }
358
- ]
359
- }
360
- },
361
- "exempt": {
362
- "type": "array",
363
- "items": {
364
- "type": "string"
365
- }
366
- }
367
- },
368
- "additionalProperties": false
369
- }
370
- ]
371
- ```
372
-
373
- **Fails**
374
-
375
- ```tsx
376
- import { Text, View } from "react-native";
377
-
378
- export const Panel = () => (
379
- <View>
380
- <Text>{"hello"}</Text>
381
- </View>
382
- );
383
- ```
384
-
385
- **Passes**
386
-
387
- ```tsx
388
- import { View } from "react-native";
389
-
390
- import { Text } from "@/components/ui/text";
391
-
392
- export const Panel = () => (
393
- <View>
394
- <Text>{"hello"}</Text>
395
- </View>
396
- );
397
- ```
398
-
399
- #### `@ashstack/core/components-tsx-only`
400
-
401
- Require every file under the components directory to render JSX or be a re-export barrel. `dir` says which directory, defaulting to `src/components`.
402
-
403
- > Off by default — opt in per project.
404
-
405
- **Options**
406
-
407
- ```jsonc
408
- [
409
- {
410
- "type": "object",
411
- "properties": {
412
- "dir": {
413
- "type": "string",
414
- "minLength": 1
415
- }
416
- },
417
- "additionalProperties": false
418
- }
419
- ]
420
- ```
421
-
422
- **Fails**
423
-
424
- ```tsx
425
- export const formatFullName = (first: string, last: string) => `${first} ${last}`;
426
- ```
427
-
428
- **Passes**
429
-
430
- ```tsx
431
- import { View } from "react-native";
432
-
433
- export const Panel = () => <View />;
434
- ```
435
-
436
- #### `@ashstack/core/hoist-intl`
437
-
438
- Disallow `new Intl.*` inside a function that renders JSX, unless the call already sits in `useMemo` or `useCallback`.
439
-
440
- **Fails**
441
-
442
- ```tsx
443
- import { View } from "react-native";
444
-
445
- export const Price = () => {
446
- const formatter = new Intl.NumberFormat("en-US");
447
- return <View accessibilityValue={{ text: formatter.format(10) }} />;
448
- };
449
- ```
450
-
451
- **Passes**
452
-
453
- ```tsx
454
- import { View } from "react-native";
455
-
456
- const formatter = new Intl.NumberFormat("en-US");
457
-
458
- export const Price = () => <View accessibilityValue={{ text: formatter.format(10) }} />;
459
- ```
460
-
461
- ### `@ashstack/zod`
462
-
463
- _auto-enabled when `zod` is a dependency._
464
-
465
- #### `@ashstack/zod/prefer-enum`
466
-
467
- Disallow `z.nativeEnum()` and any `z.union()` whose members are all `z.literal()` strings.
468
-
469
- **Fails**
470
-
471
- ```tsx
472
- import { View } from "react-native";
473
- import { z } from "zod";
474
-
475
- enum AlertType {
476
- LowMoisture = "LOW_MOISTURE",
477
- MissedDelivery = "MISSED_DELIVERY",
478
- }
479
-
480
- export const alertTypeSchema = z.nativeEnum(AlertType);
481
-
482
- export const statusSchema = z.union([z.literal("draft"), z.literal("published")]);
483
-
484
- export const Panel = () => <View />;
485
- ```
486
-
487
- **Passes**
488
-
489
- ```tsx
490
- import { View } from "react-native";
491
- import { z } from "zod";
492
-
493
- enum AlertType {
494
- LowMoisture = "LOW_MOISTURE",
495
- MissedDelivery = "MISSED_DELIVERY",
496
- }
497
-
498
- export const alertTypeSchema = z.enum(AlertType);
499
-
500
- export const statusSchema = z.enum(["draft", "published"]);
501
-
502
- export const mixedUnion = z.union([z.literal("manual"), z.number()]);
503
-
504
- export const Panel = () => <View />;
505
- ```
506
-
507
- ## `react()`
508
-
509
- React on the web. Adds the you-might-not-need-an-effect plugin (`react-effect/`) alongside oxlint's own.
510
-
511
- Plugins: `eslint`, `typescript`, `import`, `unicorn`, `promise`, `oxc`, `react`, `jsx-a11y`.
512
-
513
- ### Built-in rules
514
-
515
- | Rule | Setting |
516
- | --- | --- |
517
- | [`max-lines-per-function`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/max-lines-per-function.html) | `["error",{"max":120,"skipBlankLines":true,"skipComments":true}]` |
518
- | [`eslint/no-empty`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-empty.html) | `"off"` |
519
- | [`eslint/no-empty-function`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-empty-function.html) | `"off"` |
520
- | [`react/react-in-jsx-scope`](https://oxc.rs/docs/guide/usage/linter/rules/react/react-in-jsx-scope.html) | `"off"` |
521
- | [`react/style-prop-object`](https://oxc.rs/docs/guide/usage/linter/rules/react/style-prop-object.html) | `"off"` |
522
- | [`react/only-export-components`](https://oxc.rs/docs/guide/usage/linter/rules/react/only-export-components.html) | `"off"` |
523
- | [`react/rules-of-hooks`](https://oxc.rs/docs/guide/usage/linter/rules/react/rules-of-hooks.html) | `"error"` |
524
- | [`react/exhaustive-deps`](https://oxc.rs/docs/guide/usage/linter/rules/react/exhaustive-deps.html) | `"error"` |
525
- | [`react/button-has-type`](https://oxc.rs/docs/guide/usage/linter/rules/react/button-has-type.html) | `"error"` |
526
- | [`react/checked-requires-onchange-or-readonly`](https://oxc.rs/docs/guide/usage/linter/rules/react/checked-requires-onchange-or-readonly.html) | `"error"` |
527
- | [`react/iframe-missing-sandbox`](https://oxc.rs/docs/guide/usage/linter/rules/react/iframe-missing-sandbox.html) | `"error"` |
528
- | [`react/jsx-boolean-value`](https://oxc.rs/docs/guide/usage/linter/rules/react/jsx-boolean-value.html) | `"error"` |
529
- | [`react/jsx-key`](https://oxc.rs/docs/guide/usage/linter/rules/react/jsx-key.html) | `"error"` |
530
- | [`react/jsx-no-comment-textnodes`](https://oxc.rs/docs/guide/usage/linter/rules/react/jsx-no-comment-textnodes.html) | `"error"` |
531
- | [`react/jsx-no-constructed-context-values`](https://oxc.rs/docs/guide/usage/linter/rules/react/jsx-no-constructed-context-values.html) | `"error"` |
532
- | [`react/jsx-no-duplicate-props`](https://oxc.rs/docs/guide/usage/linter/rules/react/jsx-no-duplicate-props.html) | `"error"` |
533
- | [`react/jsx-no-target-blank`](https://oxc.rs/docs/guide/usage/linter/rules/react/jsx-no-target-blank.html) | `"error"` |
534
- | [`react/jsx-no-useless-fragment`](https://oxc.rs/docs/guide/usage/linter/rules/react/jsx-no-useless-fragment.html) | `"error"` |
535
- | [`react/no-array-index-key`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-array-index-key.html) | `"error"` |
536
- | [`react/no-children-prop`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-children-prop.html) | `"error"` |
537
- | [`react/no-danger`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-danger.html) | `"error"` |
538
- | [`react/no-danger-with-children`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-danger-with-children.html) | `"error"` |
539
- | [`react/no-direct-mutation-state`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-direct-mutation-state.html) | `"error"` |
540
- | [`react/no-is-mounted`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-is-mounted.html) | `"error"` |
541
- | [`react/no-render-return-value`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-render-return-value.html) | `"error"` |
542
- | [`react/no-string-refs`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-string-refs.html) | `"error"` |
543
- | [`react/no-this-in-sfc`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-this-in-sfc.html) | `"error"` |
544
- | [`react/no-unsafe`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-unsafe.html) | `"error"` |
545
- | [`react/no-unescaped-entities`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-unescaped-entities.html) | `"error"` |
546
- | [`react/no-unstable-nested-components`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-unstable-nested-components.html) | `["error",{"allowAsProps":true}]` |
547
- | [`react/require-render-return`](https://oxc.rs/docs/guide/usage/linter/rules/react/require-render-return.html) | `"error"` |
548
- | [`react/self-closing-comp`](https://oxc.rs/docs/guide/usage/linter/rules/react/self-closing-comp.html) | `"error"` |
549
- | [`react/void-dom-elements-no-children`](https://oxc.rs/docs/guide/usage/linter/rules/react/void-dom-elements-no-children.html) | `"error"` |
550
- | [`react/error-boundaries`](https://oxc.rs/docs/guide/usage/linter/rules/react/error-boundaries.html) | `"error"` |
551
- | [`react/globals`](https://oxc.rs/docs/guide/usage/linter/rules/react/globals.html) | `"error"` |
552
- | [`react/immutability`](https://oxc.rs/docs/guide/usage/linter/rules/react/immutability.html) | `"error"` |
553
- | [`react/incompatible-library`](https://oxc.rs/docs/guide/usage/linter/rules/react/incompatible-library.html) | `"error"` |
554
- | [`react/preserve-manual-memoization`](https://oxc.rs/docs/guide/usage/linter/rules/react/preserve-manual-memoization.html) | `"error"` |
555
- | [`react/purity`](https://oxc.rs/docs/guide/usage/linter/rules/react/purity.html) | `"error"` |
556
- | [`react/refs`](https://oxc.rs/docs/guide/usage/linter/rules/react/refs.html) | `"error"` |
557
- | [`react/set-state-in-effect`](https://oxc.rs/docs/guide/usage/linter/rules/react/set-state-in-effect.html) | `"error"` |
558
- | [`react/set-state-in-render`](https://oxc.rs/docs/guide/usage/linter/rules/react/set-state-in-render.html) | `"error"` |
559
- | [`react/static-components`](https://oxc.rs/docs/guide/usage/linter/rules/react/static-components.html) | `"error"` |
560
- | [`react/use-memo`](https://oxc.rs/docs/guide/usage/linter/rules/react/use-memo.html) | `"error"` |
561
- | [`react/void-use-memo`](https://oxc.rs/docs/guide/usage/linter/rules/react/void-use-memo.html) | `"error"` |
562
- | [`react/capitalized-calls`](https://oxc.rs/docs/guide/usage/linter/rules/react/capitalized-calls.html) | `"error"` |
563
- | [`react/exhaustive-effect-dependencies`](https://oxc.rs/docs/guide/usage/linter/rules/react/exhaustive-effect-dependencies.html) | `"error"` |
564
- | [`react/hooks`](https://oxc.rs/docs/guide/usage/linter/rules/react/hooks.html) | `"error"` |
565
- | [`react/memo-dependencies`](https://oxc.rs/docs/guide/usage/linter/rules/react/memo-dependencies.html) | `"error"` |
566
- | [`react/no-deriving-state-in-effects`](https://oxc.rs/docs/guide/usage/linter/rules/react/no-deriving-state-in-effects.html) | `"error"` |
567
- | [`react/invariant`](https://oxc.rs/docs/guide/usage/linter/rules/react/invariant.html) | `"error"` |
568
- | [`react/rule-suppression`](https://oxc.rs/docs/guide/usage/linter/rules/react/rule-suppression.html) | `"error"` |
569
- | [`react/syntax`](https://oxc.rs/docs/guide/usage/linter/rules/react/syntax.html) | `"error"` |
570
- | [`react/todo`](https://oxc.rs/docs/guide/usage/linter/rules/react/todo.html) | `"error"` |
571
- | [`react/unsupported-syntax`](https://oxc.rs/docs/guide/usage/linter/rules/react/unsupported-syntax.html) | `"error"` |
572
- | [`jsx-a11y/alt-text`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/alt-text.html) | `"error"` |
573
- | [`jsx-a11y/anchor-has-content`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/anchor-has-content.html) | `"error"` |
574
- | [`jsx-a11y/anchor-is-valid`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/anchor-is-valid.html) | `"error"` |
575
- | [`jsx-a11y/click-events-have-key-events`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/click-events-have-key-events.html) | `"off"` |
576
- | [`jsx-a11y/control-has-associated-label`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/control-has-associated-label.html) | `"off"` |
577
- | [`jsx-a11y/heading-has-content`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/heading-has-content.html) | `"error"` |
578
- | [`jsx-a11y/img-redundant-alt`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/img-redundant-alt.html) | `"error"` |
579
- | [`jsx-a11y/interactive-supports-focus`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/interactive-supports-focus.html) | `"error"` |
580
- | [`jsx-a11y/label-has-associated-control`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/label-has-associated-control.html) | `"error"` |
581
- | [`jsx-a11y/no-aria-hidden-on-focusable`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-aria-hidden-on-focusable.html) | `"error"` |
582
- | [`jsx-a11y/no-autofocus`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-autofocus.html) | `["error",{"ignoreNonDOM":true}]` |
583
- | [`jsx-a11y/no-noninteractive-element-interactions`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-noninteractive-element-interactions.html) | `"off"` |
584
- | [`jsx-a11y/no-noninteractive-tabindex`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-noninteractive-tabindex.html) | `"error"` |
585
- | [`jsx-a11y/no-redundant-roles`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-redundant-roles.html) | `"error"` |
586
- | [`jsx-a11y/no-static-element-interactions`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-static-element-interactions.html) | `"error"` |
587
- | [`jsx-a11y/prefer-tag-over-role`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/prefer-tag-over-role.html) | `"off"` |
588
- | [`jsx-a11y/role-has-required-aria-props`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/role-has-required-aria-props.html) | `"error"` |
589
- | [`jsx-a11y/role-supports-aria-props`](https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/role-supports-aria-props.html) | `"error"` |
590
- | [`react-effect/no-derived-state`](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) | `"error"` |
591
- | [`react-effect/no-chain-state-updates`](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) | `"error"` |
592
- | [`react-effect/no-event-handler`](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) | `"error"` |
593
- | [`react-effect/no-adjust-state-on-prop-change`](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) | `"error"` |
594
- | [`react-effect/no-reset-all-state-on-prop-change`](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) | `"error"` |
595
- | [`react-effect/no-pass-live-state-to-parent`](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) | `"error"` |
596
- | [`react-effect/no-pass-data-to-parent`](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) | `"error"` |
597
- | [`react-effect/no-external-store-subscription`](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) | `"error"` |
598
- | [`react-effect/no-initialize-state`](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) | `"error"` |
599
- | [`unicorn/filename-case`](https://oxc.rs/docs/guide/usage/linter/rules/unicorn/filename-case.html) | `["error",{"cases":{"kebabCase":true,"pascalCase":true}}]` |
600
-
601
- ### `@ashstack/query`
602
-
603
- _auto-enabled by `react()` when `@tanstack/react-query` is a dependency._
604
-
605
- #### `@ashstack/query/no-inline-keys`
606
-
607
- Disallow a query key written as an array literal at the call site instead of coming from a keys factory.
608
-
609
- **Fails**
610
-
611
- ```tsx
612
- import { useQuery } from "@tanstack/react-query";
613
- import { View } from "react-native";
614
-
615
- import { getPosts } from "@/api/posts/posts.requests";
616
-
617
- export const PostList = () => {
618
- const { data } = useQuery({ queryKey: ["posts", "list"], queryFn: getPosts });
619
- return <View accessible={data !== undefined} />;
620
- };
621
- ```
622
-
623
- **Passes**
624
-
625
- ```tsx
626
- import { useQuery } from "@tanstack/react-query";
627
- import { View } from "react-native";
628
-
629
- import { postKeys } from "@/api/posts/posts.keys";
630
- import { getPosts } from "@/api/posts/posts.requests";
631
-
632
- export const PostList = () => {
633
- const { data } = useQuery({ queryKey: postKeys.list(), queryFn: getPosts });
634
- return <View accessible={data !== undefined} />;
635
- };
636
- ```
637
-
638
- #### `@ashstack/query/no-deprecated-filters`
639
-
640
- Disallow the positional key argument that TanStack Query v5 removed from `invalidateQueries` and its sibling methods. The suggestion rewrites it to the filter-object form.
641
-
642
- **Fails**
643
-
644
- ```tsx
645
- import { useQueryClient } from "@tanstack/react-query";
646
- import { View } from "react-native";
647
-
648
- export const RefreshPanel = () => {
649
- const queryClient = useQueryClient();
650
- const refresh = () => queryClient.invalidateQueries(["posts"]);
651
- return <View onLayout={refresh} />;
652
- };
653
- ```
654
-
655
- **Passes**
656
-
657
- ```tsx
658
- import { useQueryClient } from "@tanstack/react-query";
659
- import { View } from "react-native";
660
-
661
- import { postKeys } from "@/api/posts/posts.keys";
662
-
663
- export const RefreshPanel = () => {
664
- const queryClient = useQueryClient();
665
- const refresh = () => queryClient.invalidateQueries({ queryKey: postKeys.all() });
666
- return <View onLayout={refresh} />;
667
- };
668
- ```
669
-
670
- #### `@ashstack/query/require-destructured-hooks`
671
-
672
- Require destructuring the result of a hook imported from an `@/api/*.queries` or `@/api/*.mutations` module.
673
-
674
- **Fails**
675
-
676
- ```tsx
677
- import { View } from "react-native";
678
-
679
- import { usePostsQuery } from "@/api/posts/posts.queries";
680
-
681
- export const PostList = () => {
682
- const posts = usePostsQuery();
683
- return <View accessible={posts.isSuccess} />;
684
- };
685
- ```
686
-
687
- **Passes**
688
-
689
- ```tsx
690
- import { View } from "react-native";
691
-
692
- import { usePostsQuery } from "@/api/posts/posts.queries";
693
-
694
- export const PostList = () => {
695
- const { isSuccess } = usePostsQuery();
696
- return <View accessible={isSuccess} />;
697
- };
698
- ```
699
-
700
- #### `@ashstack/query/no-fetch-in-query-fn`
701
-
702
- Disallow a bare `fetch(` inside a `queryFn` or `mutationFn`.
703
-
704
- **Fails**
705
-
706
- ```tsx
707
- import { useQuery } from "@tanstack/react-query";
708
- import { View } from "react-native";
709
-
710
- import { postKeys } from "@/api/posts/posts.keys";
711
-
712
- export const PostList = () => {
713
- const { data } = useQuery({
714
- queryKey: postKeys.list(),
715
- queryFn: () => fetch("/api/posts").then(response => response.json()),
716
- });
717
- return <View accessible={data !== undefined} />;
718
- };
719
- ```
720
-
721
- **Passes**
722
-
723
- ```tsx
724
- import { useQuery } from "@tanstack/react-query";
725
- import { View } from "react-native";
726
-
727
- import { postKeys } from "@/api/posts/posts.keys";
728
- import { getPosts } from "@/api/posts/posts.requests";
729
-
730
- export const PostList = () => {
731
- const { data } = useQuery({ queryKey: postKeys.list(), queryFn: getPosts });
732
- return <View accessible={data !== undefined} />;
733
- };
734
- ```
735
-
736
- #### `@ashstack/query/next-page-param-undefined`
737
-
738
- Disallow `return null` in the body of a `getNextPageParam`.
739
-
740
- **Fails**
741
-
742
- ```tsx
743
- import { useInfiniteQuery } from "@tanstack/react-query";
744
- import { View } from "react-native";
745
-
746
- import { postKeys } from "@/api/posts/posts.keys";
747
- import { getPosts } from "@/api/posts/posts.requests";
748
-
749
- export const PostFeed = () => {
750
- const { data } = useInfiniteQuery({
751
- queryKey: postKeys.list(),
752
- queryFn: getPosts,
753
- initialPageParam: 0,
754
- getNextPageParam: lastPage => {
755
- if (!lastPage.nextCursor) return null;
756
- return lastPage.nextCursor;
757
- },
758
- });
759
- return <View accessible={data !== undefined} />;
760
- };
761
- ```
762
-
763
- **Passes**
764
-
765
- ```tsx
766
- import { useInfiniteQuery } from "@tanstack/react-query";
767
- import { View } from "react-native";
768
-
769
- import { postKeys } from "@/api/posts/posts.keys";
770
- import { getPosts } from "@/api/posts/posts.requests";
771
-
772
- export const PostFeed = () => {
773
- const { data } = useInfiniteQuery({
774
- queryKey: postKeys.list(),
775
- queryFn: getPosts,
776
- initialPageParam: 0,
777
- getNextPageParam: lastPage => {
778
- if (!lastPage.nextCursor) return undefined;
779
- return lastPage.nextCursor;
780
- },
781
- });
782
- return <View accessible={data !== undefined} />;
783
- };
784
- ```
785
-
786
- ### `@ashstack/zustand`
787
-
788
- _auto-enabled by `react()` when `zustand` is a dependency._
789
-
790
- #### `@ashstack/zustand/require-selector`
791
-
792
- Require a selector on a store hook, rather than no arguments or `undefined` in its place.
793
-
794
- **Fails**
795
-
796
- ```tsx
797
- import { View } from "react-native";
798
-
799
- import { useSettingsStore } from "@/stores/settings-store";
800
-
801
- export const Panel = () => {
802
- const settings = useSettingsStore();
803
- return <View accessible={settings.ready} />;
804
- };
805
- ```
806
-
807
- **Passes**
808
-
809
- ```tsx
810
- import { View } from "react-native";
811
-
812
- import { useSettingsStore } from "@/stores/settings-store";
813
-
814
- export const Panel = () => {
815
- const ready = useSettingsStore(state => state.ready);
816
- return <View accessible={ready} />;
817
- };
818
- ```
819
-
820
- ### `@ashstack/i18n`
821
-
822
- _auto-enabled by `react()` when an i18n library (i18next, lingui, react-intl, use-intl, next-intl, expo-localization) is a dependency._
823
-
824
- #### `@ashstack/i18n/no-bare-text`
825
-
826
- Disallow a JSX element that carries no attributes and whose single child is plain literal text.
827
-
828
- **Fails**
829
-
830
- ```tsx
831
- import { Text } from "@/components/ui/text";
832
-
833
- export const Greeting = () => <Text>Hello there</Text>;
834
- ```
835
-
836
- **Passes**
837
-
838
- ```tsx
839
- import { useTranslation } from "react-i18next";
840
-
841
- import { Text } from "@/components/ui/text";
842
-
843
- export const Greeting = () => {
844
- const { t } = useTranslation();
845
- return <Text>{t("greeting")}</Text>;
846
- };
847
- ```
848
-
849
- #### `@ashstack/i18n/no-bare-attrs`
850
-
851
- Disallow a plain string literal on a configurable list of user-visible JSX attributes, defaulting to placeholder, accessibilityLabel, accessibilityHint and title.
852
-
853
- **Options**
854
-
855
- ```jsonc
856
- [
857
- {
858
- "type": "object",
859
- "properties": {
860
- "attributes": {
861
- "type": "array",
862
- "items": {
863
- "type": "string"
864
- },
865
- "minItems": 1
866
- }
867
- },
868
- "additionalProperties": false
869
- }
870
- ]
871
- ```
872
-
873
- **Fails**
874
-
875
- ```tsx
876
- import { TextInput } from "react-native";
877
-
878
- export const NameField = () => <TextInput placeholder="Your name" />;
879
- ```
880
-
881
- **Passes**
882
-
883
- ```tsx
884
- import { useTranslation } from "react-i18next";
885
- import { TextInput } from "react-native";
886
-
887
- export const NameField = () => {
888
- const { t } = useTranslation();
889
- return <TextInput placeholder={t("name")} />;
890
- };
891
- ```
892
-
893
- #### `@ashstack/i18n/no-bare-toast`
894
-
895
- Disallow a `toast.*` call whose only argument is a string literal.
896
-
897
- **Fails**
898
-
899
- ```tsx
900
- import { toast } from "sonner-native";
901
-
902
- import { Button } from "@/components/ui/button";
903
-
904
- export const SaveButton = () => <Button onPress={() => toast.success("Saved")} />;
905
- ```
906
-
907
- **Passes**
908
-
909
- ```tsx
910
- import { useTranslation } from "react-i18next";
911
- import { toast } from "sonner-native";
912
-
913
- import { Button } from "@/components/ui/button";
914
-
915
- export const SaveButton = () => {
916
- const { t } = useTranslation();
917
- return <Button onPress={() => toast.success(t("saved"))} />;
918
- };
919
- ```
920
-
921
- ## `react-native()`
922
-
923
- Expo and React Native.
924
-
925
- Plugins: `eslint`, `typescript`, `import`, `unicorn`, `promise`, `oxc`, `react`, `jsx-a11y`.
926
-
927
- ### Built-in rules
928
-
929
- | Rule | Setting |
930
- | --- | --- |
931
- | [`eslint/no-empty`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-empty.html) | `"error"` |
932
- | [`eslint/no-empty-function`](https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-empty-function.html) | `"error"` |
933
- | [`react-effect/no-event-handler`](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) | `"off"` |
934
-
935
- ### `@ashstack/react-native`
936
-
937
- _always on via `reactNative()`._
938
-
939
- **Import bans that ship with this module**
940
-
941
- - `forwardRef` from `react`
942
- - `useContext` from `react`
943
- - any import of `expo-linear-gradient`, `react-native-linear-gradient`
944
-
945
- #### `@ashstack/react-native/no-keyboard-will-events`
946
-
947
- Disallow the `keyboardWill*` event names. They are iOS-only, so on Android the listener registers and never fires.
948
-
949
- **Fails**
950
-
951
- ```tsx
952
- import { Keyboard } from "react-native";
953
-
954
- const onKeyboard = () => undefined;
955
-
956
- export function subscribeBad() {
957
- return Keyboard.addListener("keyboardWillShow", onKeyboard);
958
- }
959
- ```
960
-
961
- **Passes**
962
-
963
- ```tsx
964
- import { Keyboard } from "react-native";
965
-
966
- const onKeyboard = () => undefined;
967
-
968
- export function subscribeGood() {
969
- return Keyboard.addListener("keyboardDidShow", onKeyboard);
970
- }
971
- ```
972
-
973
- #### `@ashstack/react-native/no-scroll-position-state`
974
-
975
- Disallow a React state setter inside a scroll handler prop. Scroll fires every frame, and so would the re-render.
976
-
977
- **Fails**
978
-
979
- ```tsx
980
- import { useState } from "react";
981
- import { ScrollView } from "react-native";
982
-
983
- export function BadScroll() {
984
- const [, setOffset] = useState(0);
985
- return <ScrollView onScroll={event => setOffset(event.nativeEvent.contentOffset.y)} />;
986
- }
987
- ```
988
-
989
- **Passes**
990
-
991
- ```tsx
992
- import Animated, { useAnimatedScrollHandler, useSharedValue } from "react-native-reanimated";
993
-
994
- export function GoodScroll() {
995
- const offset = useSharedValue(0);
996
- const onScroll = useAnimatedScrollHandler(event => {
997
- offset.set(event.contentOffset.y);
998
- });
999
- return <Animated.ScrollView onScroll={onScroll} />;
1000
- }
1001
- ```
1002
-
1003
- #### `@ashstack/react-native/no-conditional-style-array`
1004
-
1005
- Disallow conditional and logical entries inside a JSX `style` array. A falsy entry leaves a hole that breaks the Unistyles C++ proxy.
1006
-
1007
- **Fails**
1008
-
1009
- ```tsx
1010
- import { View } from "react-native";
1011
- import { StyleSheet } from "react-native-unistyles";
1012
-
1013
- export function BadStyleArray({ active }: { active: boolean }) {
1014
- return <View style={[styles.card, active && styles.cardActive]} />;
1015
- }
1016
-
1017
- const styles = StyleSheet.create({ card: {}, cardActive: {} });
1018
- ```
1019
-
1020
- **Passes**
1021
-
1022
- ```tsx
1023
- import { View } from "react-native";
1024
- import { StyleSheet } from "react-native-unistyles";
1025
-
1026
- export function GoodStyleArray({ active }: { active: boolean }) {
1027
- return <View style={styles.card(active)} />;
1028
- }
1029
-
1030
- const styles = StyleSheet.create({
1031
- card: (active: boolean) => ({ opacity: active ? 1 : 0.5 }),
1032
- });
1033
- ```
1034
-
1035
- #### `@ashstack/react-native/no-leaked-render`
1036
-
1037
- Disallow a `&&` guard in JSX on a value this rule can see is a number or a string — a `.length`, a `.size`, arithmetic, a literal. The falsy left operand leaks into the output, and a bare `0` crashes React Native with "Text strings must be rendered within a <Text> component".
1038
-
1039
- **Fails**
1040
-
1041
- ```tsx
1042
- import { Text, View } from "react-native";
1043
-
1044
- export function BadLeakedRender({ items }: { items: string[] }) {
1045
- return <View>{items.length && <Text>Has items</Text>}</View>;
1046
- }
1047
- ```
1048
-
1049
- **Passes**
1050
-
1051
- ```tsx
1052
- import { Text, View } from "react-native";
1053
-
1054
- export function GoodLeakedRender({ items }: { items: string[] }) {
1055
- return <View>{items.length > 0 && <Text>Has items</Text>}</View>;
1056
- }
1057
- ```
1058
-
1059
- #### `@ashstack/react-native/no-rn-image-network-source`
1060
-
1061
- Disallow a react-native `<Image>` with a network `{ uri }` source. It has no disk cache and no decode sizing, so the image re-downloads at full size on every cold start.
1062
-
1063
- **Fails**
1064
-
1065
- ```tsx
1066
- import { Image } from "react-native";
1067
-
1068
- export function BadNetworkImage({ uri }: { uri: string }) {
1069
- return <Image source={{ uri }} />;
1070
- }
1071
- ```
1072
-
1073
- **Passes**
1074
-
1075
- ```tsx
1076
- import { Image } from "react-native";
1077
- import { TurboImage } from "react-native-turbo-image";
1078
-
1079
- const logo = require("./logo.png");
1080
-
1081
- export function GoodLocalImage() {
1082
- return <Image source={logo} />;
1083
- }
1084
-
1085
- export function GoodNetworkImage({ uri }: { uri: string }) {
1086
- return <TurboImage source={{ uri }} resize={40} cachePolicy="dataCache" />;
1087
- }
1088
- ```
1089
-
1090
- #### `@ashstack/react-native/no-redundant-view-nesting`
1091
-
1092
- Disallow a View or Animated.View that wraps an identical view when neither carries anything but a style. Every extra host view is a real node in the native tree.
1093
-
1094
- **Fails**
1095
-
1096
- ```tsx
1097
- import { View } from "react-native";
1098
- import { StyleSheet } from "react-native-unistyles";
1099
-
1100
- export function BadNesting() {
1101
- return (
1102
- <View style={styles.outer}>
1103
- <View style={styles.inner} />
1104
- </View>
1105
- );
1106
- }
1107
-
1108
- const styles = StyleSheet.create({ outer: {}, inner: {} });
1109
- ```
1110
-
1111
- **Passes**
1112
-
1113
- ```tsx
1114
- import { Text, View } from "react-native";
1115
- import { StyleSheet } from "react-native-unistyles";
1116
-
1117
- export function GoodNesting() {
1118
- return (
1119
- <View style={styles.merged}>
1120
- <Text>One host view instead of two</Text>
1121
- </View>
1122
- );
1123
- }
1124
-
1125
- const styles = StyleSheet.create({ merged: {} });
1126
- ```
1127
-
1128
- #### `@ashstack/react-native/no-rn-namespace-import`
1129
-
1130
- Disallow a namespace import of react-native and a re-export of its `Platform`. Both defeat Metro's platform shaking, so dead `Platform.OS` branches ship in both bundles.
1131
-
1132
- **Fails**
1133
-
1134
- ```tsx
1135
- import * as RN from "react-native";
1136
-
1137
- export const BadNamespace = () => <RN.View />;
1138
- ```
1139
-
1140
- **Passes**
1141
-
1142
- ```tsx
1143
- import { View } from "react-native";
1144
-
1145
- export const GoodNamespace = () => <View />;
1146
- ```
1147
-
1148
- #### `@ashstack/react-native/no-unlabeled-icon-pressable`
1149
-
1150
- Require an accessible name on an icon-only touchable or an icon-only Expo UI `<Button>`. Without a label, hint or visible text, a screen reader cannot reach the control.
1151
-
1152
- **Fails**
1153
-
1154
- ```tsx
1155
- import { Pressable } from "react-native-gesture-handler";
1156
- import { StarIcon } from "./icons";
1157
-
1158
- export function BadIconButton({ onPress }: { onPress: () => void }) {
1159
- return (
1160
- <Pressable onPress={onPress}>
1161
- <StarIcon />
1162
- </Pressable>
1163
- );
1164
- }
1165
- ```
1166
-
1167
- **Passes**
1168
-
1169
- ```tsx
1170
- import { Pressable } from "react-native-gesture-handler";
1171
- import { StarIcon } from "./icons";
1172
-
1173
- export function GoodIconButton({ onPress }: { onPress: () => void }) {
1174
- return (
1175
- <Pressable accessibilityLabel="Add to favourites" onPress={onPress}>
1176
- <StarIcon />
1177
- </Pressable>
1178
- );
1179
- }
1180
- ```
1181
-
1182
- #### `@ashstack/react-native/hoist-stateless-function`
1183
-
1184
- Require module scope for a non-component function that reads nothing from the component around it. Out there it is created once, keeps a stable identity, and can be tested without rendering.
1185
-
1186
- **Fails**
1187
-
1188
- ```tsx
1189
- import { Text } from "react-native";
1190
-
1191
- export function BadHoist() {
1192
- const formatLabel = (count: number) => `${count} items`;
1193
- return <Text>{formatLabel(3)}</Text>;
1194
- }
1195
- ```
1196
-
1197
- **Passes**
1198
-
1199
- ```tsx
1200
- import { Text } from "react-native";
1201
-
1202
- const formatLabel = (count: number) => `${count} items`;
1203
-
1204
- export function GoodHoist({ count }: { count: number }) {
1205
- const label = () => formatLabel(count);
1206
- return <Text>{label()}</Text>;
1207
- }
1208
- ```
1209
-
1210
- #### `@ashstack/react-native/no-manual-memo`
1211
-
1212
- Disallow `useMemo`, `useCallback` and `memo` unless a `why:` comment on the line above states the case the React Compiler cannot see: something rendered per list row, or a cost that was measured.
1213
-
1214
- > Enabled only when one of `babel-plugin-react-compiler`, `react-compiler-runtime`, `react-compiler-marker` is a dependency.
1215
-
1216
- **Fails**
1217
-
1218
- ```tsx
1219
- import { useMemo } from "react";
1220
-
1221
- export function useBadTotal(items: number[]) {
1222
- return useMemo(() => items.reduce((sum, item) => sum + item, 0), [items]);
1223
- }
1224
- ```
1225
-
1226
- **Passes**
1227
-
1228
- ```tsx
1229
- import { useMemo } from "react";
1230
-
1231
- export function useGoodTotal(items: number[]) {
1232
- // why: measured - this sum runs per row in a 10k-item list
1233
- return useMemo(() => items.reduce((sum, item) => sum + item, 0), [items]);
1234
- }
1235
- ```
1236
-
1237
- #### `@ashstack/react-native/no-dynamic-import`
1238
-
1239
- Disallow dynamic `import()` outside a `React.lazy` or `dynamic` wrapper. Metro inlines it into the same bundle, so nothing is deferred and the module is hidden from the typechecker.
1240
-
1241
- **Fails**
1242
-
1243
- ```tsx
1244
- import { View } from "react-native";
1245
-
1246
- export const loadHelpers = () => import("./helpers");
1247
-
1248
- export const Panel = () => <View />;
1249
- ```
1250
-
1251
- **Passes**
1252
-
1253
- ```tsx
1254
- import { lazy } from "react";
1255
- import { View } from "react-native";
1256
-
1257
- export const Details = lazy(() => import("./details"));
1258
-
1259
- export const Panel = () => <View />;
1260
- ```
1261
-
1262
- ### `@ashstack/unistyles`
1263
-
1264
- _auto-enabled when `react-native-unistyles` is a dependency._
1265
-
1266
- **Import bans that ship with this module**
1267
-
1268
- - `StyleSheet` from `react-native`
1269
- - `useWindowDimensions`, `Dimensions` from `react-native`
1270
- - `SafeAreaView` from `react-native`
1271
- - `useSafeAreaInsets`, `useSafeAreaFrame`, `SafeAreaView` from `react-native-safe-area-context`
1272
-
1273
- #### `@ashstack/unistyles/animated-theme`
1274
-
1275
- A Reanimated worklet hook cannot see `useUnistyles()` theme changes on the UI thread. Read the shared value from `useAnimatedTheme()` there instead.
1276
-
1277
- **Fails**
1278
-
1279
- ```tsx
1280
- import Animated, { useAnimatedStyle } from "react-native-reanimated";
1281
- import { useUnistyles } from "react-native-unistyles";
1282
-
1283
- export function Fade() {
1284
- const { theme } = useUnistyles();
1285
- const animatedStyle = useAnimatedStyle(() => ({ backgroundColor: theme.colors.accent }));
1286
-
1287
- return <Animated.View style={animatedStyle} />;
1288
- }
1289
- ```
1290
-
1291
- **Passes**
1292
-
1293
- ```tsx
1294
- import Animated, { useAnimatedStyle } from "react-native-reanimated";
1295
- import { useAnimatedTheme } from "react-native-unistyles/reanimated";
1296
-
1297
- export function Fade() {
1298
- const animatedTheme = useAnimatedTheme();
1299
- const animatedStyle = useAnimatedStyle(() => ({ backgroundColor: animatedTheme.value.colors.accent }));
1300
-
1301
- return <Animated.View style={animatedStyle} />;
1302
- }
1303
- ```
1304
-
1305
- #### `@ashstack/unistyles/content-container`
1306
-
1307
- A raw component never subscribes its `contentContainerStyle` to theme or `rt` updates. Wrap the component with `withUnistyles` when the style depends on either.
1308
-
1309
- **Fails**
1310
-
1311
- ```tsx
1312
- import { ScrollView } from "react-native";
1313
- import { StyleSheet } from "react-native-unistyles";
1314
-
1315
- export function List() {
1316
- return <ScrollView contentContainerStyle={styles.content} />;
1317
- }
1318
-
1319
- const styles = StyleSheet.create((theme, rt) => ({
1320
- content: { paddingBottom: rt.insets.bottom },
1321
- }));
1322
- ```
1323
-
1324
- **Passes**
1325
-
1326
- ```tsx
1327
- import { ScrollView } from "react-native";
1328
- import { StyleSheet, withUnistyles } from "react-native-unistyles";
1329
-
1330
- const UnistylesScrollView = withUnistyles(ScrollView);
1331
-
1332
- export function List() {
1333
- return <UnistylesScrollView contentContainerStyle={styles.content} />;
1334
- }
1335
-
1336
- const styles = StyleSheet.create((theme, rt) => ({
1337
- content: { paddingBottom: rt.insets.bottom },
1338
- }));
1339
- ```
1340
-
1341
- #### `@ashstack/unistyles/in-sheet`
1342
-
1343
- Inside `StyleSheet.create`, require `rt` and theme values rather than Dimensions, PixelRatio, Appearance, I18nManager, StatusBar or `UnistylesRuntime`. It also covers logical spacing, `boxShadow`, `borderCurve` and a redundant `as const`.
1344
-
1345
- **Fails**
1346
-
1347
- ```tsx
1348
- import { Dimensions, I18nManager } from "react-native";
1349
- import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
1350
-
1351
- export const styles = StyleSheet.create(theme => ({
1352
- card: {
1353
- position: "absolute" as const,
1354
- borderRadius: theme.radius.md,
1355
- shadowColor: theme.colors.shadow,
1356
- paddingLeft: theme.spacing[4],
1357
- width: Dimensions.get("window").width,
1358
- flexDirection: I18nManager.isRTL ? "row-reverse" : "row",
1359
- paddingBottom: UnistylesRuntime.insets.bottom,
1360
- },
1361
- }));
1362
- ```
1363
-
1364
- **Passes**
1365
-
1366
- ```tsx
1367
- import { StyleSheet } from "react-native-unistyles";
1368
-
1369
- export const styles = StyleSheet.create((theme, rt) => ({
1370
- card: {
1371
- position: "absolute",
1372
- borderRadius: theme.radius.md,
1373
- borderCurve: "continuous",
1374
- boxShadow: theme.shadows.card,
1375
- paddingStart: theme.spacing[4],
1376
- width: rt.screen.width,
1377
- flexDirection: rt.rtl ? "row-reverse" : "row",
1378
- paddingBottom: rt.insets.bottom,
1379
- },
1380
- }));
1381
- ```
1382
-
1383
- #### `@ashstack/unistyles/insets`
1384
-
1385
- Disallow passing `useSafeAreaInsets()` values into a dynamic style function or an inline JSX style object. Read `rt.insets` inside `StyleSheet.create` instead.
1386
-
1387
- **Fails**
1388
-
1389
- ```tsx
1390
- import { View } from "react-native";
1391
- import { useSafeAreaInsets } from "react-native-safe-area-context";
1392
- import { StyleSheet } from "react-native-unistyles";
1393
-
1394
- export function Screen() {
1395
- const insets = useSafeAreaInsets();
1396
-
1397
- return <View style={[styles.footer(insets.bottom), { paddingTop: insets.top }]} />;
1398
- }
1399
-
1400
- const styles = StyleSheet.create({
1401
- footer: (bottom: number) => ({ paddingBottom: bottom }),
1402
- });
1403
- ```
1404
-
1405
- **Passes**
1406
-
1407
- ```tsx
1408
- import { View } from "react-native";
1409
- import { StyleSheet } from "react-native-unistyles";
1410
-
1411
- export function Screen() {
1412
- return <View style={styles.footer} />;
1413
- }
1414
-
1415
- const styles = StyleSheet.create((theme, rt) => ({
1416
- footer: { paddingBottom: rt.insets.bottom },
1417
- }));
1418
- ```
1419
-
1420
- #### `@ashstack/unistyles/no-hardcoded-color`
1421
-
1422
- Disallow hex and CSS-function colors inside `StyleSheet.create`. A raw color skips dark mode and never changes with the theme.
1423
-
1424
- **Fails**
1425
-
1426
- ```tsx
1427
- import { StyleSheet } from "react-native-unistyles";
1428
-
1429
- export const styles = StyleSheet.create({
1430
- raw: { color: "#FFFFFF" },
1431
- translucent: { backgroundColor: "rgba(255, 255, 255, 0.24)" },
1432
- });
1433
- ```
1434
-
1435
- **Passes**
1436
-
1437
- ```tsx
1438
- import { StyleSheet } from "react-native-unistyles";
1439
-
1440
- export const styles = StyleSheet.create(theme => ({
1441
- tinted: { color: theme.colors.text, backgroundColor: theme.colors.surface },
1442
- gradient: { experimental_backgroundImage: theme.gradients.header },
1443
- }));
1444
- ```
1445
-
1446
- #### `@ashstack/unistyles/no-hardcoded-spacing`
1447
-
1448
- Require `theme.spacing` or `theme.sizing.scale` for spacing, radius and type values inside `StyleSheet.create`, instead of raw numbers.
1449
-
1450
- **Fails**
1451
-
1452
- ```tsx
1453
- import { StyleSheet } from "react-native-unistyles";
1454
-
1455
- export const styles = StyleSheet.create({
1456
- card: {
1457
- padding: 8,
1458
- gap: 4,
1459
- borderRadius: 12,
1460
- borderCurve: "continuous",
1461
- fontSize: 16,
1462
- },
1463
- });
1464
- ```
1465
-
1466
- **Passes**
1467
-
1468
- ```tsx
1469
- import { StyleSheet } from "react-native-unistyles";
1470
-
1471
- const CALIBRATED_GUTTER = 14;
1472
-
1473
- export const styles = StyleSheet.create(theme => ({
1474
- card: {
1475
- padding: theme.spacing[2],
1476
- gap: theme.sizing.scale(4),
1477
- paddingEnd: 0,
1478
- paddingHorizontal: CALIBRATED_GUTTER,
1479
- borderRadius: theme.radius.md,
1480
- borderCurve: "continuous",
1481
- fontSize: theme.typography.body.size,
1482
- },
1483
- }));
1484
- ```
1485
-
1486
- #### `@ashstack/unistyles/no-margin`
1487
-
1488
- Disallow non-negative `margin` inside `StyleSheet.create`; `gap` on the parent or `padding` on the element spaces children without leaving a hole when one is removed.
1489
-
1490
- **Fails**
1491
-
1492
- ```tsx
1493
- import { StyleSheet } from "react-native-unistyles";
1494
-
1495
- export const styles = StyleSheet.create(theme => ({
1496
- spaced: { marginTop: theme.spacing[2] },
1497
- sided: { marginHorizontal: theme.spacing[6] },
1498
- }));
1499
- ```
1500
-
1501
- **Passes**
1502
-
1503
- ```tsx
1504
- import { StyleSheet } from "react-native-unistyles";
1505
-
1506
- const STACK_OVERLAP = 12;
1507
-
1508
- export const styles = StyleSheet.create(theme => ({
1509
- row: { gap: theme.spacing[2], paddingHorizontal: theme.spacing[4] },
1510
- overlapped: { marginStart: -STACK_OVERLAP },
1511
- }));
1512
- ```
1513
-
1514
- #### `@ashstack/unistyles/no-style-spread`
1515
-
1516
- Disallow spreading a stylesheet style into another object. The spread reads through the Unistyles C++ proxy once, so the result stops reacting to the theme.
1517
-
1518
- **Fails**
1519
-
1520
- ```tsx
1521
- import { View } from "react-native";
1522
- import { StyleSheet } from "react-native-unistyles";
1523
-
1524
- export function Card() {
1525
- return <View style={{ ...styles.card, opacity: 1 }} />;
1526
- }
1527
-
1528
- const styles = StyleSheet.create(theme => ({
1529
- card: { backgroundColor: theme.colors.surface },
1530
- }));
1531
- ```
1532
-
1533
- **Passes**
1534
-
1535
- ```tsx
1536
- import { View } from "react-native";
1537
- import { StyleSheet } from "react-native-unistyles";
1538
-
1539
- export function Card() {
1540
- return <View style={[styles.card, styles.raised]} />;
1541
- }
1542
-
1543
- const styles = StyleSheet.create(theme => ({
1544
- card: { backgroundColor: theme.colors.surface },
1545
- raised: { boxShadow: theme.shadows.card },
1546
- }));
1547
- ```
1548
-
1549
- #### `@ashstack/unistyles/no-unused-styles`
1550
-
1551
- Disallow stylesheet keys that nothing in the file reads. A computed key, a computed read, or a sheet that leaves the module skips the whole file.
1552
-
1553
- **Fails**
1554
-
1555
- ```tsx
1556
- import { View } from "react-native";
1557
- import { StyleSheet } from "react-native-unistyles";
1558
-
1559
- export function Card() {
1560
- return <View style={styles.card} />;
1561
- }
1562
-
1563
- const styles = StyleSheet.create(theme => ({
1564
- card: { backgroundColor: theme.colors.surface },
1565
- orphan: { backgroundColor: theme.colors.accent },
1566
- }));
1567
- ```
1568
-
1569
- **Passes**
1570
-
1571
- ```tsx
1572
- import { View } from "react-native";
1573
- import { StyleSheet } from "react-native-unistyles";
1574
-
1575
- export function Card() {
1576
- return <View style={styles.card} />;
1577
- }
1578
-
1579
- const styles = StyleSheet.create(theme => ({
1580
- card: { backgroundColor: theme.colors.surface },
1581
- }));
1582
- ```
1583
-
1584
- #### `@ashstack/unistyles/rtl-style-call`
1585
-
1586
- Disallow passing `I18nManager.isRTL` from JSX into a dynamic style function. Unistyles tracks the dependency itself once the style reads `rt.rtl`.
1587
-
1588
- **Fails**
1589
-
1590
- ```tsx
1591
- import { I18nManager, View } from "react-native";
1592
- import { StyleSheet } from "react-native-unistyles";
1593
-
1594
- export function Row() {
1595
- return <View style={styles.row(I18nManager.isRTL)} />;
1596
- }
1597
-
1598
- const styles = StyleSheet.create({
1599
- row: (rtl: boolean) => ({ transform: [{ scaleX: rtl ? -1 : 1 }] }),
1600
- });
1601
- ```
1602
-
1603
- **Passes**
1604
-
1605
- ```tsx
1606
- import { View } from "react-native";
1607
- import { StyleSheet } from "react-native-unistyles";
1608
-
1609
- export function Row() {
1610
- return <View style={styles.row} />;
1611
- }
1612
-
1613
- const styles = StyleSheet.create((theme, rt) => ({
1614
- row: { flexDirection: rt.rtl ? "row-reverse" : "row" },
1615
- }));
1616
- ```
1617
-
1618
- #### `@ashstack/unistyles/theme-screen-component`
1619
-
1620
- `theme.screen.*` is a snapshot taken at module initialization, so a component that reads it never sees the current size. `useUnistyles().rt.screen` and `useWindowDimensions` do.
1621
-
1622
- **Fails**
1623
-
1624
- ```tsx
1625
- import { View } from "react-native";
1626
- import { useUnistyles } from "react-native-unistyles";
1627
-
1628
- export function Panel() {
1629
- const { theme } = useUnistyles();
1630
-
1631
- return <View accessibilityLabel={String(theme.screen.width)} />;
1632
- }
1633
-
1634
- export const Card = () => {
1635
- const { theme } = useUnistyles();
1636
-
1637
- return <View accessibilityLabel={String(theme.screen.height)} />;
1638
- };
1639
- ```
1640
-
1641
- **Passes**
1642
-
1643
- ```tsx
1644
- import { View } from "react-native";
1645
- import { useUnistyles } from "react-native-unistyles";
1646
-
1647
- export function Panel() {
1648
- const { rt } = useUnistyles();
1649
-
1650
- return <View accessibilityLabel={String(rt.screen.width)} />;
1651
- }
1652
- ```
1653
-
1654
- #### `@ashstack/unistyles/theme-style-attr`
1655
-
1656
- Theme-dependent values belong in `StyleSheet.create`, not in a JSX `style` prop that reads a `useUnistyles()` theme.
1657
-
1658
- **Fails**
1659
-
1660
- ```tsx
1661
- import { View } from "react-native";
1662
- import { useUnistyles } from "react-native-unistyles";
1663
-
1664
- export function Badge() {
1665
- const { theme } = useUnistyles();
1666
-
1667
- return <View style={{ backgroundColor: theme.colors.accent }} />;
1668
- }
1669
-
1670
- export function BadgeList({ ids }: { ids: string[] }) {
1671
- const { theme } = useUnistyles();
1672
-
1673
- return <>{ids.map(id => <View key={id} style={{ backgroundColor: theme.colors.accent }} />)}</>;
1674
- }
1675
- ```
1676
-
1677
- **Passes**
1678
-
1679
- ```tsx
1680
- import { View } from "react-native";
1681
- import { StyleSheet } from "react-native-unistyles";
1682
-
1683
- export function Badge() {
1684
- return <View style={styles.badge} />;
1685
- }
1686
-
1687
- const styles = StyleSheet.create(theme => ({
1688
- badge: { backgroundColor: theme.colors.accent },
1689
- }));
1690
- ```
1691
-
1692
- ### `@ashstack/legend-list`
1693
-
1694
- _auto-enabled when `@legendapp/list` is a dependency._
1695
-
1696
- **Import bans that ship with this module**
1697
-
1698
- - `FlatList`, `SectionList`, `VirtualizedList` from `react-native`
1699
- - `*` from `@shopify/flash-list`
1700
- - any import of `@legendapp/list`
1701
-
1702
- #### `@ashstack/legend-list/required-props`
1703
-
1704
- Require `keyExtractor` and an explicit `recycleItems` on a Legend List. Without a key extractor the list keys its rows by index.
1705
-
1706
- **Fails**
1707
-
1708
- ```tsx
1709
- import { LegendList } from "@legendapp/list/react-native";
1710
-
1711
- declare const items: { id: string }[];
1712
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1713
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1714
-
1715
- // keyExtractor and recycleItems are both missing.
1716
- export const Bad = () => <LegendList data={items} renderItem={renderRow} />;
1717
- ```
1718
-
1719
- **Passes**
1720
-
1721
- ```tsx
1722
- import { LegendList } from "@legendapp/list/react-native";
1723
-
1724
- declare const items: { id: string }[];
1725
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1726
- const keyOf = (item: { id: string }) => item.id;
1727
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1728
-
1729
- export const Good = () => (
1730
- <LegendList data={items} renderItem={renderRow} keyExtractor={keyOf} recycleItems />
1731
- );
1732
-
1733
- // An explicit opt-out is a decision, not a violation.
1734
- export const NoRecycle = () => (
1735
- <LegendList data={items} renderItem={renderRow} keyExtractor={keyOf} recycleItems={false} />
1736
- );
1737
- ```
1738
-
1739
- #### `@ashstack/legend-list/no-index-key-extractor`
1740
-
1741
- Disallow a `keyExtractor` that uses its index parameter. Cached sizes and recycled row state hang off the key, so a prepend points every measurement at the wrong item.
1742
-
1743
- **Fails**
1744
-
1745
- ```tsx
1746
- import { LegendList } from "@legendapp/list/react-native";
1747
-
1748
- declare const items: { id: string }[];
1749
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1750
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1751
-
1752
- export const Bad = () => (
1753
- <LegendList
1754
- data={items}
1755
- renderItem={renderRow}
1756
- keyExtractor={(item, index) => String(index)}
1757
- recycleItems
1758
- />
1759
- );
1760
- ```
1761
-
1762
- **Passes**
1763
-
1764
- ```tsx
1765
- import { LegendList } from "@legendapp/list/react-native";
1766
-
1767
- declare const items: { id: string }[];
1768
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1769
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1770
-
1771
- export const Good = () => (
1772
- <LegendList data={items} renderItem={renderRow} keyExtractor={item => item.id} recycleItems />
1773
- );
1774
- ```
1775
-
1776
- #### `@ashstack/legend-list/no-remount-key`
1777
-
1778
- Disallow `key` on a Legend List, which remounts on any key change and loses its measurements and scroll position. Pass `dataKey` instead.
1779
-
1780
- **Fails**
1781
-
1782
- ```tsx
1783
- import { LegendList } from "@legendapp/list/react-native";
1784
-
1785
- declare const items: { id: string }[];
1786
- declare const activeTab: string;
1787
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1788
- const keyOf = (item: { id: string }) => item.id;
1789
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1790
-
1791
- export const Bad = () => (
1792
- <LegendList key={activeTab} data={items} renderItem={renderRow} keyExtractor={keyOf} recycleItems />
1793
- );
1794
- ```
1795
-
1796
- **Passes**
1797
-
1798
- ```tsx
1799
- import { LegendList } from "@legendapp/list/react-native";
1800
-
1801
- declare const items: { id: string }[];
1802
- declare const activeTab: string;
1803
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1804
- const keyOf = (item: { id: string }) => item.id;
1805
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1806
-
1807
- export const Good = () => (
1808
- <LegendList dataKey={activeTab} data={items} renderItem={renderRow} keyExtractor={keyOf} recycleItems />
1809
- );
1810
- ```
1811
-
1812
- #### `@ashstack/legend-list/no-inline-data`
1813
-
1814
- Disallow building a Legend List's `data` inline. Each render produces a new array reference, and the list re-diffs, re-keys and drops what it had cached.
1815
-
1816
- **Fails**
1817
-
1818
- ```tsx
1819
- import { LegendList } from "@legendapp/list/react-native";
1820
-
1821
- declare const items: { id: string; hidden: boolean }[];
1822
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1823
- const keyOf = (item: { id: string }) => item.id;
1824
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1825
-
1826
- export const BadCall = () => (
1827
- <LegendList data={items.filter(item => !item.hidden)} renderItem={renderRow} keyExtractor={keyOf} recycleItems />
1828
- );
1829
-
1830
- export const BadFallback = () => (
1831
- <LegendList data={items ?? []} renderItem={renderRow} keyExtractor={keyOf} recycleItems />
1832
- );
1833
- ```
1834
-
1835
- **Passes**
1836
-
1837
- ```tsx
1838
- import { LegendList } from "@legendapp/list/react-native";
1839
- import { useMemo } from "react";
1840
-
1841
- declare const rawItems: { id: string; hidden: boolean }[];
1842
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1843
- const keyOf = (item: { id: string }) => item.id;
1844
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1845
-
1846
- export const Good = () => {
1847
- const visible = useMemo(() => rawItems.filter(item => !item.hidden), []);
1848
- return <LegendList data={visible} renderItem={renderRow} keyExtractor={keyOf} recycleItems />;
1849
- };
1850
- ```
1851
-
1852
- #### `@ashstack/legend-list/no-inline-extra-data`
1853
-
1854
- An inline object or array as `extraData` takes a new identity on every parent render, and every mounted row re-evaluates with it.
1855
-
1856
- **Fails**
1857
-
1858
- ```tsx
1859
- import { LegendList } from "@legendapp/list/react-native";
1860
-
1861
- declare const items: { id: string }[];
1862
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1863
- const keyOf = (item: { id: string }) => item.id;
1864
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1865
-
1866
- export const Bad = () => (
1867
- <LegendList
1868
- data={items}
1869
- renderItem={renderRow}
1870
- keyExtractor={keyOf}
1871
- recycleItems
1872
- extraData={{ live: true }}
1873
- />
1874
- );
1875
- ```
1876
-
1877
- **Passes**
1878
-
1879
- ```tsx
1880
- import { LegendList } from "@legendapp/list/react-native";
1881
-
1882
- declare const items: { id: string }[];
1883
- declare const selectedId: string;
1884
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1885
- const keyOf = (item: { id: string }) => item.id;
1886
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1887
-
1888
- export const Good = () => (
1889
- <LegendList data={items} renderItem={renderRow} keyExtractor={keyOf} recycleItems extraData={selectedId} />
1890
- );
1891
- ```
1892
-
1893
- #### `@ashstack/legend-list/no-inline-render-item-props`
1894
-
1895
- Disallow inline object and array literals on props nested inside `renderItem`. A row whose props take a new identity every render can never be skipped.
1896
-
1897
- **Fails**
1898
-
1899
- ```tsx
1900
- import { LegendList } from "@legendapp/list/react-native";
1901
-
1902
- declare const items: { id: string }[];
1903
- declare const Row: (props: { item: { id: string }; style?: unknown }) => JSX.Element;
1904
- const keyOf = (item: { id: string }) => item.id;
1905
-
1906
- export const Bad = () => (
1907
- <LegendList
1908
- data={items}
1909
- renderItem={({ item }) => <Row item={item} style={{ paddingVertical: 8 }} />}
1910
- keyExtractor={keyOf}
1911
- recycleItems
1912
- />
1913
- );
1914
- ```
1915
-
1916
- **Passes**
1917
-
1918
- ```tsx
1919
- import { LegendList } from "@legendapp/list/react-native";
1920
-
1921
- declare const items: { id: string }[];
1922
- declare const Row: (props: { item: { id: string }; gap?: number }) => JSX.Element;
1923
- const keyOf = (item: { id: string }) => item.id;
1924
-
1925
- export const Good = () => (
1926
- <LegendList
1927
- data={items}
1928
- renderItem={({ item }) => <Row item={item} gap={8} />}
1929
- keyExtractor={keyOf}
1930
- recycleItems
1931
- />
1932
- );
1933
- ```
1934
-
1935
- #### `@ashstack/legend-list/no-mixed-children`
1936
-
1937
- Disallow passing both `data` and real children to a Legend List. The combination is unsupported and one of the two is dropped without a warning.
1938
-
1939
- **Fails**
1940
-
1941
- ```tsx
1942
- import { LegendList } from "@legendapp/list/react-native";
1943
-
1944
- declare const items: { id: string }[];
1945
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1946
- declare const Footer: () => JSX.Element;
1947
- const keyOf = (item: { id: string }) => item.id;
1948
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1949
-
1950
- export const Bad = () => (
1951
- <LegendList data={items} renderItem={renderRow} keyExtractor={keyOf} recycleItems>
1952
- <Footer />
1953
- </LegendList>
1954
- );
1955
- ```
1956
-
1957
- **Passes**
1958
-
1959
- ```tsx
1960
- import { LegendList } from "@legendapp/list/react-native";
1961
-
1962
- declare const items: { id: string }[];
1963
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1964
- declare const Footer: () => JSX.Element;
1965
- const keyOf = (item: { id: string }) => item.id;
1966
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1967
-
1968
- // data mode
1969
- export const DataMode = () => (
1970
- <LegendList data={items} renderItem={renderRow} keyExtractor={keyOf} recycleItems />
1971
- );
1972
-
1973
- // children mode
1974
- export const ChildrenMode = () => (
1975
- <LegendList keyExtractor={keyOf} recycleItems>
1976
- <Footer />
1977
- </LegendList>
1978
- );
1979
- ```
1980
-
1981
- #### `@ashstack/legend-list/no-flex-in-content-container`
1982
-
1983
- Disallow `flex` in a Legend List's `contentContainerStyle`, where it sizes the scrolled content to the viewport and the list ends up measuring zero height.
1984
-
1985
- **Fails**
1986
-
1987
- ```tsx
1988
- import { LegendList } from "@legendapp/list/react-native";
1989
- import { StyleSheet } from "react-native-unistyles";
1990
-
1991
- declare const items: { id: string }[];
1992
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
1993
- const keyOf = (item: { id: string }) => item.id;
1994
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
1995
-
1996
- export const Bad = () => (
1997
- <LegendList
1998
- data={items}
1999
- renderItem={renderRow}
2000
- keyExtractor={keyOf}
2001
- recycleItems
2002
- contentContainerStyle={styles.stretched}
2003
- />
2004
- );
2005
-
2006
- const styles = StyleSheet.create(theme => {
2007
- if (theme.dark) return { stretched: { flex: 1 } };
2008
- return { stretched: { padding: theme.gap } };
2009
- });
2010
- ```
2011
-
2012
- **Passes**
2013
-
2014
- ```tsx
2015
- import { LegendList } from "@legendapp/list/react-native";
2016
- import { StyleSheet } from "react-native-unistyles";
2017
-
2018
- declare const items: { id: string }[];
2019
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
2020
- const keyOf = (item: { id: string }) => item.id;
2021
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
2022
-
2023
- export const Good = () => (
2024
- <LegendList
2025
- data={items}
2026
- renderItem={renderRow}
2027
- keyExtractor={keyOf}
2028
- recycleItems
2029
- style={styles.fill}
2030
- contentContainerStyle={styles.content}
2031
- />
2032
- );
2033
-
2034
- const styles = StyleSheet.create({
2035
- fill: { flex: 1 },
2036
- content: { paddingBottom: 24 },
2037
- });
2038
- ```
2039
-
2040
- #### `@ashstack/legend-list/typed-items-need-item-type`
2041
-
2042
- Require `getItemType` when a row branches on `item.type`. Without it every layout shares one recycling pool and one size average.
2043
-
2044
- **Fails**
2045
-
2046
- ```tsx
2047
- import { LegendList } from "@legendapp/list/react-native";
2048
-
2049
- declare const items: { id: string; type: "header" | "photo" }[];
2050
- declare const Header: () => JSX.Element;
2051
- declare const Photo: () => JSX.Element;
2052
- const keyOf = (item: { id: string }) => item.id;
2053
- const renderMixed = ({ item }: { item: { type: string } }) =>
2054
- item.type === "header" ? <Header /> : <Photo />;
2055
-
2056
- export const Bad = () => (
2057
- <LegendList data={items} renderItem={renderMixed} keyExtractor={keyOf} recycleItems />
2058
- );
2059
- ```
2060
-
2061
- **Passes**
2062
-
2063
- ```tsx
2064
- import { LegendList } from "@legendapp/list/react-native";
2065
-
2066
- declare const items: { id: string; type: "header" | "photo" }[];
2067
- declare const Header: () => JSX.Element;
2068
- declare const Photo: () => JSX.Element;
2069
- const keyOf = (item: { id: string }) => item.id;
2070
- const renderMixed = ({ item }: { item: { type: string } }) =>
2071
- item.type === "header" ? <Header /> : <Photo />;
2072
-
2073
- export const Good = () => (
2074
- <LegendList
2075
- data={items}
2076
- renderItem={renderMixed}
2077
- keyExtractor={keyOf}
2078
- recycleItems
2079
- getItemType={item => item.type}
2080
- />
2081
- );
2082
- ```
2083
-
2084
- #### `@ashstack/legend-list/no-scrollview-map`
2085
-
2086
- A `ScrollView` mounts every child up front, so a mapped collection does not belong in its children.
2087
-
2088
- **Fails**
2089
-
2090
- ```tsx
2091
- import { ScrollView } from "react-native";
2092
-
2093
- declare const items: { id: string }[];
2094
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
2095
-
2096
- export const Bad = () => (
2097
- <ScrollView>
2098
- {items.map(item => (
2099
- <Row key={item.id} item={item} />
2100
- ))}
2101
- </ScrollView>
2102
- );
2103
- ```
2104
-
2105
- **Passes**
2106
-
2107
- ```tsx
2108
- import { ScrollView } from "react-native";
2109
-
2110
- declare const Header: () => JSX.Element;
2111
- declare const Footer: () => JSX.Element;
2112
-
2113
- // A handful of static children is what a ScrollView is for.
2114
- export const Good = () => (
2115
- <ScrollView>
2116
- <Header />
2117
- <Footer />
2118
- </ScrollView>
2119
- );
2120
- ```
2121
-
2122
- #### `@ashstack/legend-list/no-unsupported-props`
2123
-
2124
- Disallow FlashList and FlatList props that Legend List v3 does not have. It ignores them rather than rejecting them, so the feature looks broken.
2125
-
2126
- **Fails**
2127
-
2128
- ```tsx
2129
- import { LegendList } from "@legendapp/list/react-native";
2130
-
2131
- declare const items: { id: string }[];
2132
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
2133
- const keyOf = (item: { id: string }) => item.id;
2134
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
2135
-
2136
- export const Bad = () => (
2137
- <LegendList data={items} renderItem={renderRow} keyExtractor={keyOf} recycleItems inverted masonry />
2138
- );
2139
- ```
2140
-
2141
- **Passes**
2142
-
2143
- ```tsx
2144
- import { LegendList } from "@legendapp/list/react-native";
2145
-
2146
- declare const items: { id: string }[];
2147
- declare const Row: (props: { item: { id: string } }) => JSX.Element;
2148
- const keyOf = (item: { id: string }) => item.id;
2149
- const renderRow = ({ item }: { item: { id: string } }) => <Row item={item} />;
2150
-
2151
- export const Good = () => (
2152
- <LegendList
2153
- data={items}
2154
- renderItem={renderRow}
2155
- keyExtractor={keyOf}
2156
- recycleItems
2157
- maintainScrollAtEnd
2158
- initialScrollAtEnd
2159
- />
2160
- );
2161
- ```
2162
-
2163
- ### `@ashstack/legend-state`
2164
-
2165
- _auto-enabled when `@legendapp/state` is a dependency._
2166
-
2167
- **Import bans that ship with this module**
2168
-
2169
- - `use$`, `useSelector` from `@legendapp/state/react`
2170
-
2171
- #### `@ashstack/legend-state/no-assignment`
2172
-
2173
- Write an observable with `.set(...)` or `.assign({...})`. Assigning or incrementing it with an operator is a silent no-op.
2174
-
2175
- **Fails**
2176
-
2177
- ```tsx
2178
- import { observable } from "@legendapp/state";
2179
-
2180
- const settings$ = observable({ theme: "light", count: 0 });
2181
-
2182
- export const applyDark = () => {
2183
- settings$.theme = "dark";
2184
- };
2185
-
2186
- export const bump = () => {
2187
- settings$.count++;
2188
- };
2189
- ```
2190
-
2191
- **Passes**
2192
-
2193
- ```tsx
2194
- import { observable } from "@legendapp/state";
2195
-
2196
- const settings$ = observable({ theme: "light", count: 0 });
2197
-
2198
- export const applyDark = () => {
2199
- settings$.theme.set("dark");
2200
- };
2201
-
2202
- export const bump = () => {
2203
- settings$.count.set(value => value + 1);
2204
- };
2205
- ```
2206
-
2207
- #### `@ashstack/legend-state/naming`
2208
-
2209
- A variable initialized from `observable()` or `useObservable()` needs a trailing `$`. The other rules in this module key off that suffix.
2210
-
2211
- **Fails**
2212
-
2213
- ```tsx
2214
- import { observable } from "@legendapp/state";
2215
-
2216
- export const counter = observable(0);
2217
- ```
2218
-
2219
- **Passes**
2220
-
2221
- ```tsx
2222
- import { observable } from "@legendapp/state";
2223
-
2224
- export const counter$ = observable(0);
2225
- ```
2226
-
2227
- #### `@ashstack/legend-state/no-nested-observable`
2228
-
2229
- Never pass an observable to `observable()` or `useObservable()`. The wrapper is a second node, and reads and writes on it never reach the original.
2230
-
2231
- **Fails**
2232
-
2233
- ```tsx
2234
- import { observable } from "@legendapp/state";
2235
-
2236
- export const count$ = observable(0);
2237
-
2238
- export const wrapped$ = observable(count$);
2239
- ```
2240
-
2241
- **Passes**
2242
-
2243
- ```tsx
2244
- import { observable } from "@legendapp/state";
2245
-
2246
- export const count$ = observable(0);
2247
-
2248
- export const doubled$ = observable(() => count$.get() * 2);
2249
- ```
2250
-
2251
- #### `@ashstack/legend-state/no-react-mirror`
2252
-
2253
- Seeding `useState` from an observable's `get()` or `peek()` gives the value two owners. Read it with `useValue(...)` where it renders.
2254
-
2255
- **Fails**
2256
-
2257
- ```tsx
2258
- import { observable } from "@legendapp/state";
2259
- import { useState } from "react";
2260
-
2261
- const count$ = observable(0);
2262
-
2263
- export const useCounter = () => {
2264
- const [count] = useState(count$.get());
2265
- return count;
2266
- };
2267
- ```
2268
-
2269
- **Passes**
2270
-
2271
- ```tsx
2272
- import { observable } from "@legendapp/state";
2273
- import { useValue } from "@legendapp/state/react";
2274
-
2275
- const count$ = observable(0);
2276
-
2277
- export const useCounter = () => useValue(count$);
2278
- ```
2279
-
2280
- #### `@ashstack/legend-state/no-untracked-get-in-jsx`
2281
-
2282
- A `get()` placed directly in a JSX expression container is a plain read. The value renders once and never updates.
2283
-
2284
- **Fails**
2285
-
2286
- ```tsx
2287
- import { observable } from "@legendapp/state";
2288
- import { View } from "react-native";
2289
-
2290
- const count$ = observable(0);
2291
-
2292
- export const Counter = () => <View accessibilityValue={{ now: count$.get() }} />;
2293
- ```
2294
-
2295
- **Passes**
2296
-
2297
- ```tsx
2298
- import { observable } from "@legendapp/state";
2299
- import { Memo } from "@legendapp/state/react";
2300
- import { View } from "react-native";
2301
-
2302
- const count$ = observable(0);
2303
-
2304
- export const Counter = () => (
2305
- <View>
2306
- <Memo>{() => count$.get()}</Memo>
2307
- </View>
2308
- );
2309
- ```
2310
-
2311
- #### `@ashstack/legend-state/no-peek-in-selector`
2312
-
2313
- `peek()` never subscribes, so a selector or tracking callback that uses it never re-runs. Call `get()` there and keep `peek()` for handlers.
2314
-
2315
- **Fails**
2316
-
2317
- ```tsx
2318
- import { observable } from "@legendapp/state";
2319
- import { useValue } from "@legendapp/state/react";
2320
-
2321
- const settings$ = observable({ theme: "light" });
2322
-
2323
- export const useTheme = () => useValue(() => settings$.theme.peek());
2324
- ```
2325
-
2326
- **Passes**
2327
-
2328
- ```tsx
2329
- import { observable } from "@legendapp/state";
2330
- import { useValue } from "@legendapp/state/react";
2331
-
2332
- const settings$ = observable({ theme: "light" });
2333
-
2334
- export const useTheme = () => useValue(() => settings$.theme.get());
2335
-
2336
- export const readThemeOnce = () => settings$.theme.peek();
2337
- ```
2338
-
2339
- #### `@ashstack/legend-state/no-object-selector`
2340
-
2341
- A `useValue` selector that builds a new object or array returns a fresh identity every run. The component then re-renders on every store change.
2342
-
2343
- **Fails**
2344
-
2345
- ```tsx
2346
- import { observable } from "@legendapp/state";
2347
- import { useValue } from "@legendapp/state/react";
2348
-
2349
- const settings$ = observable({ theme: "light", locale: "en" });
2350
-
2351
- export const useSettings = () =>
2352
- useValue(() => ({ theme: settings$.theme.get(), locale: settings$.locale.get() }));
2353
- ```
2354
-
2355
- **Passes**
2356
-
2357
- ```tsx
2358
- import { observable } from "@legendapp/state";
2359
- import { useValue } from "@legendapp/state/react";
2360
-
2361
- const settings$ = observable({ theme: "light", locale: "en" });
2362
-
2363
- export const useTheme = () => useValue(() => settings$.theme.get());
2364
-
2365
- export const useLocale = () => useValue(() => settings$.locale.get());
2366
- ```
2367
-
2368
- #### `@ashstack/legend-state/no-observable-in-component`
2369
-
2370
- Calling `observable()` inside a component or hook makes a new observable on every render. Use `useObservable()` or a module-level store instead.
2371
-
2372
- **Fails**
2373
-
2374
- ```tsx
2375
- import { observable } from "@legendapp/state";
2376
- import { View } from "react-native";
2377
-
2378
- export const Counter = () => {
2379
- const count$ = observable(0);
2380
- return <View accessible={count$ !== undefined} />;
2381
- };
2382
- ```
2383
-
2384
- **Passes**
2385
-
2386
- ```tsx
2387
- import { useObservable } from "@legendapp/state/react";
2388
- import { View } from "react-native";
2389
-
2390
- export const Counter = () => {
2391
- const count$ = useObservable(0);
2392
- return <View accessible={count$ !== undefined} />;
2393
- };
2394
- ```
2395
-
2396
- ### `@ashstack/reanimated`
2397
-
2398
- _auto-enabled when `react-native-reanimated` is a dependency._
2399
-
2400
- **Import bans that ship with this module**
2401
-
2402
- - `Animated` from `react-native`
2403
- - `runOnJS`, `runOnUI` from `react-native-reanimated`
2404
- - `runOnJS`, `runOnUI` from `react-native-worklets`
2405
-
2406
- #### `@ashstack/reanimated/animated-reaction-safety`
2407
-
2408
- A `useAnimatedReaction` result callback loops forever if it writes a shared value the prepare callback reads. Guard `scheduleOnRN` there on the current result differing from the previous one.
2409
-
2410
- **Fails**
2411
-
2412
- ```tsx
2413
- import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
2414
- import { scheduleOnRN } from "react-native-worklets";
2415
-
2416
- declare function report(value: number): void;
2417
-
2418
- export function BadReaction() {
2419
- const progress = useSharedValue(0);
2420
-
2421
- useAnimatedReaction(
2422
- () => progress.get(),
2423
- current => {
2424
- progress.set(current + 1);
2425
- scheduleOnRN(report, current);
2426
- }
2427
- );
2428
-
2429
- return null;
2430
- }
2431
- ```
2432
-
2433
- **Passes**
2434
-
2435
- ```tsx
2436
- import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
2437
- import { scheduleOnRN } from "react-native-worklets";
2438
-
2439
- declare function report(value: number): void;
2440
-
2441
- export function GoodReaction() {
2442
- const progress = useSharedValue(0);
2443
- const mirror = useSharedValue(0);
2444
-
2445
- useAnimatedReaction(
2446
- () => progress.get() > 0.5,
2447
- (current, previous) => {
2448
- if (current !== previous) {
2449
- mirror.set(current ? 1 : 0);
2450
- scheduleOnRN(report, current ? 1 : 0);
2451
- }
2452
- }
2453
- );
2454
-
2455
- return null;
2456
- }
2457
- ```
2458
-
2459
- #### `@ashstack/reanimated/animated-style-needs-animated-component`
2460
-
2461
- An animated style only takes effect on an `Animated.*` component. A plain element applies it once at mount and then never updates.
2462
-
2463
- **Fails**
2464
-
2465
- ```tsx
2466
- import { View } from "react-native";
2467
- import { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
2468
-
2469
- export function BadAnimatedStyleHost() {
2470
- const progress = useSharedValue(0);
2471
- const fade = useAnimatedStyle(() => ({ opacity: progress.get() }));
2472
- return <View style={fade} />;
2473
- }
2474
- ```
2475
-
2476
- **Passes**
2477
-
2478
- ```tsx
2479
- import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
2480
-
2481
- export function GoodAnimatedStyleHost() {
2482
- const progress = useSharedValue(0);
2483
- const fade = useAnimatedStyle(() => ({ opacity: progress.get() }));
2484
- return <Animated.View style={fade} />;
2485
- }
2486
- ```
2487
-
2488
- #### `@ashstack/reanimated/animated-updater-purity`
2489
-
2490
- An updater passed to `useAnimatedStyle` or `useAnimatedProps` must stay pure, so it may not write a shared value or call `scheduleOnRN`.
2491
-
2492
- **Fails**
2493
-
2494
- ```tsx
2495
- import { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
2496
- import { scheduleOnRN } from "react-native-worklets";
2497
-
2498
- declare function report(value: number): void;
2499
-
2500
- export function useBadUpdater() {
2501
- const progress = useSharedValue(0);
2502
-
2503
- return useAnimatedStyle(() => {
2504
- progress.set(progress.get() + 1);
2505
- scheduleOnRN(report, progress.get());
2506
- return { opacity: progress.get() };
2507
- });
2508
- }
2509
- ```
2510
-
2511
- **Passes**
2512
-
2513
- ```tsx
2514
- import { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
2515
-
2516
- export function useGoodUpdater() {
2517
- const progress = useSharedValue(0);
2518
- return useAnimatedStyle(() => ({ opacity: progress.get() }));
2519
- }
2520
- ```
2521
-
2522
- #### `@ashstack/reanimated/gpu-properties-only`
2523
-
2524
- Animate `transform` and `opacity` in `useAnimatedStyle` and `useAnimatedProps`. Layout properties such as `width` or `margin` recalculate layout every frame.
2525
-
2526
- **Fails**
2527
-
2528
- ```tsx
2529
- import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
2530
-
2531
- export function BadGpu() {
2532
- const progress = useSharedValue(0);
2533
- const grow = useAnimatedStyle(() => ({ height: progress.get() * 100 }));
2534
- return <Animated.View style={grow} />;
2535
- }
2536
- ```
2537
-
2538
- **Passes**
2539
-
2540
- ```tsx
2541
- import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
2542
-
2543
- export function GoodGpu() {
2544
- const progress = useSharedValue(0);
2545
- const slide = useAnimatedStyle(() => ({
2546
- opacity: progress.get(),
2547
- transform: [{ translateY: progress.get() * 10 }],
2548
- }));
2549
- return <Animated.View style={slide} />;
2550
- }
2551
- ```
2552
-
2553
- #### `@ashstack/reanimated/hoist-layout-animation-builder`
2554
-
2555
- A layout animation belongs at module scope, or inside a memo when it depends on component values. The `entering`/`exiting`/`layout` props otherwise rebuild it on every render.
2556
-
2557
- **Fails**
2558
-
2559
- ```tsx
2560
- import Animated, { FadeIn } from "react-native-reanimated";
2561
-
2562
- export function BadLayoutBuilder() {
2563
- return <Animated.View entering={FadeIn.duration(250)} />;
2564
- }
2565
- ```
2566
-
2567
- **Passes**
2568
-
2569
- ```tsx
2570
- import Animated, { FadeIn } from "react-native-reanimated";
2571
-
2572
- const FADE_IN = FadeIn.duration(250);
2573
-
2574
- export function GoodLayoutBuilder() {
2575
- return <Animated.View entering={FADE_IN} />;
2576
- }
2577
- ```
2578
-
2579
- #### `@ashstack/reanimated/interpolate-needs-clamp`
2580
-
2581
- Give `interpolate()` a fourth argument that clamps. Without one, or with `EXTEND` or `IDENTITY`, the output keeps going past the ends of the input range.
2582
-
2583
- **Fails**
2584
-
2585
- ```tsx
2586
- import Animated, { interpolate, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
2587
-
2588
- export function BadInterpolate() {
2589
- const offset = useSharedValue(0);
2590
- const fade = useAnimatedStyle(() => ({ opacity: interpolate(offset.get(), [0, 100], [1, 0]) }));
2591
- return <Animated.View style={fade} />;
2592
- }
2593
- ```
2594
-
2595
- **Passes**
2596
-
2597
- ```tsx
2598
- import Animated, { Extrapolation, interpolate, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
2599
-
2600
- export function GoodInterpolate() {
2601
- const offset = useSharedValue(0);
2602
- const fade = useAnimatedStyle(() => ({
2603
- opacity: interpolate(offset.get(), [0, 100], [1, 0], Extrapolation.CLAMP),
2604
- }));
2605
- return <Animated.View style={fade} />;
2606
- }
2607
- ```
2608
-
2609
- #### `@ashstack/reanimated/no-shared-value-dot-value`
2610
-
2611
- Shared values are read with `.get()` and written with `.set(...)`, never through `.value`. React Compiler cannot track a `.value` access.
2612
-
2613
- **Fails**
2614
-
2615
- ```tsx
2616
- import { useSharedValue } from "react-native-reanimated";
2617
-
2618
- export function useBadDotValue() {
2619
- const progress = useSharedValue(0);
2620
- progress.value = 1;
2621
- return progress.value;
2622
- }
2623
- ```
2624
-
2625
- **Passes**
2626
-
2627
- ```tsx
2628
- import { useSharedValue } from "react-native-reanimated";
2629
-
2630
- export function useGoodDotValue() {
2631
- const progress = useSharedValue(0);
2632
- progress.set(1);
2633
- return progress.get();
2634
- }
2635
- ```
2636
-
2637
- #### `@ashstack/reanimated/no-react-state-from-continuous-worklet`
2638
-
2639
- A worklet that runs every frame must not send a React state setter through `scheduleOnRN`. That puts a Fabric commit on an animation frame.
2640
-
2641
- **Fails**
2642
-
2643
- ```tsx
2644
- import { useState } from "react";
2645
- import { useFrameCallback } from "react-native-reanimated";
2646
- import { scheduleOnRN } from "react-native-worklets";
2647
-
2648
- export function BadWorkletState() {
2649
- const [, setVisible] = useState(false);
2650
-
2651
- useFrameCallback(() => {
2652
- scheduleOnRN(setVisible, true);
2653
- });
2654
-
2655
- return null;
2656
- }
2657
- ```
2658
-
2659
- **Passes**
2660
-
2661
- ```tsx
2662
- import { useFrameCallback, useSharedValue } from "react-native-reanimated";
2663
-
2664
- export function GoodWorkletState() {
2665
- const visible = useSharedValue(false);
2666
-
2667
- useFrameCallback(() => {
2668
- visible.set(true);
2669
- });
2670
-
2671
- return null;
2672
- }
2673
- ```
2674
-
2675
- #### `@ashstack/reanimated/prefer-lazy-shared-value-initializer`
2676
-
2677
- Pass a computed `useSharedValue` initial value as a function. An eager call runs on every render while only the first result is kept.
2678
-
2679
- **Fails**
2680
-
2681
- ```tsx
2682
- import { useSharedValue } from "react-native-reanimated";
2683
-
2684
- declare function computeInitialProgress(): number;
2685
-
2686
- export function useBadInitializer() {
2687
- return useSharedValue(computeInitialProgress());
2688
- }
2689
- ```
2690
-
2691
- **Passes**
2692
-
2693
- ```tsx
2694
- import { useSharedValue } from "react-native-reanimated";
2695
-
2696
- declare function computeInitialProgress(): number;
2697
-
2698
- export function useGoodInitializer() {
2699
- return useSharedValue(() => computeInitialProgress());
2700
- }
2701
- ```
2702
-
2703
- #### `@ashstack/reanimated/schedule-on-rn-scope`
2704
-
2705
- `scheduleOnRN` takes a function declared in RN Runtime scope. An inline callback can end up created on the wrong runtime.
2706
-
2707
- **Fails**
2708
-
2709
- ```tsx
2710
- import { useFrameCallback } from "react-native-reanimated";
2711
- import { scheduleOnRN } from "react-native-worklets";
2712
-
2713
- declare function report(value: number): void;
2714
-
2715
- export function useBadSchedule() {
2716
- useFrameCallback(() => {
2717
- scheduleOnRN(() => report(1));
2718
- });
2719
- }
2720
- ```
2721
-
2722
- **Passes**
2723
-
2724
- ```tsx
2725
- import { useFrameCallback } from "react-native-reanimated";
2726
- import { scheduleOnRN } from "react-native-worklets";
2727
-
2728
- declare function report(value: number): void;
2729
-
2730
- export function useGoodSchedule() {
2731
- useFrameCallback(() => {
2732
- scheduleOnRN(report, 1);
2733
- });
2734
- }
2735
- ```
2736
-
2737
- #### `@ashstack/reanimated/shared-value-usage`
2738
-
2739
- Destructuring a shared value or mutating what its `get()` returned detaches it from Reanimated reactivity. Reading or writing one while JSX evaluates also makes render impure.
2740
-
2741
- **Fails**
2742
-
2743
- ```tsx
2744
- import { Text } from "react-native";
2745
- import { useSharedValue } from "react-native-reanimated";
2746
-
2747
- export function BadSharedValueUsage() {
2748
- const progress = useSharedValue(0);
2749
- return <Text>{progress.get()}</Text>;
2750
- }
2751
- ```
2752
-
2753
- **Passes**
2754
-
2755
- ```tsx
2756
- import { Text } from "react-native";
2757
- import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
2758
-
2759
- export function GoodSharedValueUsage() {
2760
- const progress = useSharedValue(0);
2761
- const fade = useAnimatedStyle(() => ({ opacity: progress.get() }));
2762
- return (
2763
- <Animated.View style={fade}>
2764
- <Text>Loading</Text>
2765
- </Animated.View>
2766
- );
2767
- }
2768
- ```
2769
-
2770
- ### `@ashstack/turbo-image`
2771
-
2772
- _auto-enabled when `react-native-turbo-image` is a dependency._
2773
-
2774
- #### `@ashstack/turbo-image/require-resize`
2775
-
2776
- Require `resize` on a TurboImage so the native decoder downsamples before the bitmap reaches memory. A full-resolution decode wastes tens of megabytes and stalls the first frame.
2777
-
2778
- **Fails**
2779
-
2780
- ```tsx
2781
- import { TurboImage } from "react-native-turbo-image";
2782
-
2783
- export function BadTurboResize({ uri }: { uri: string }) {
2784
- return <TurboImage source={{ uri }} cachePolicy="dataCache" />;
2785
- }
2786
- ```
2787
-
2788
- **Passes**
2789
-
2790
- ```tsx
2791
- import { TurboImage } from "react-native-turbo-image";
2792
-
2793
- export function GoodTurboResize({ uri }: { uri: string }) {
2794
- return <TurboImage source={{ uri }} resize={40} cachePolicy="dataCache" />;
2795
- }
2796
- ```
2797
-
2798
- #### `@ashstack/turbo-image/require-cache-policy`
2799
-
2800
- Require `cachePolicy` on a TurboImage. Without one the image is re-fetched over the network on every cold start, so an already-scrolled feed costs its bandwidth again.
2801
-
2802
- **Fails**
2803
-
2804
- ```tsx
2805
- import { TurboImage } from "react-native-turbo-image";
2806
-
2807
- export function BadTurboCache({ uri }: { uri: string }) {
2808
- return <TurboImage source={{ uri }} resize={40} />;
2809
- }
2810
- ```
2811
-
2812
- **Passes**
2813
-
2814
- ```tsx
2815
- import { TurboImage } from "react-native-turbo-image";
2816
-
2817
- export function GoodTurboCache({ uri }: { uri: string }) {
2818
- return <TurboImage source={{ uri }} resize={40} cachePolicy="dataCache" />;
2819
- }
2820
- ```
2821
-
2822
- ### `@ashstack/skia`
2823
-
2824
- _auto-enabled when `@shopify/react-native-skia` is a dependency._
2825
-
2826
- **Import bans that ship with this module**
2827
-
2828
- - `usePathValue`, `usePathInterpolation` from `@shopify/react-native-skia`
2829
-
2830
- #### `@ashstack/skia/canvas-opaque`
2831
-
2832
- Require an explicit `opaque` prop on a Skia `<Canvas>`. A fullscreen animated canvas wants it on; anything that needs transparency or a view transform wants it off.
2833
-
2834
- **Fails**
2835
-
2836
- ```tsx
2837
- import { Canvas } from "@shopify/react-native-skia";
2838
-
2839
- export function BadCanvas() {
2840
- return <Canvas />;
2841
- }
2842
- ```
2843
-
2844
- **Passes**
2845
-
2846
- ```tsx
2847
- import { Canvas } from "@shopify/react-native-skia";
2848
- import { Platform } from "react-native";
2849
-
2850
- export function GoodCanvas() {
2851
- return <Canvas opaque={Platform.OS === "android"} />;
2852
- }
2853
- ```
2854
-
2855
- ### `@ashstack/keyboard`
2856
-
2857
- _auto-enabled when `react-native-keyboard-controller` is a dependency._
2858
-
2859
- **Import bans that ship with this module**
2860
-
2861
- - `KeyboardAvoidingView` from `react-native`
2862
-
2863
- ### Import bans without a module
2864
-
2865
- _Auto-detected from your dependencies; these carry no rules of their own._
2866
-
2867
- **`react-native-gesture-handler` installed**
2868
-
2869
- - `Pressable`, `TouchableOpacity`, `TouchableHighlight`, `TouchableWithoutFeedback`, `TouchableNativeFeedback` from `react-native`
2870
-
2871
- **`expo-router` installed**
2872
-
2873
- - any import of `@react-navigation/stack`, `@react-navigation/bottom-tabs`
2874
-
2875
- **`expo-font` installed**
2876
-
2877
- - `useFonts`, `loadAsync` from `expo-font`
2878
-
2879
- **`react-native-quick-crypto` installed**
2880
-
2881
- - `*` from `crypto-js`