Skip to content

Commit fb0ca63

Browse files
authored
add allowErrorBoundary option (#29)
* add allowGetDerivedStateFromError option * switch to allowErrorBoundary
1 parent c9d5d79 commit fb0ca63

3 files changed

Lines changed: 117 additions & 30 deletions

File tree

README.md

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ using only functions.
3434

3535
Leaving the choice between class or function components up to the community is great, but generally within a codebase I want consistency: either we're writing class components and HoCs or we're using function components and hooks. Straddling the two adds unnecessary hurdles for sharing reusable logic.
3636

37-
By default, class components that use `componentDidCatch` are enabled because there is currently no hook alternative for React. This option is configurable via `allowComponentDidCatch`.
37+
By default, class components that use `componentDidCatch` or `static getDerivedStateFromError` are allowed because there is currently no hook alternative for React. This is configurable via `allowErrorBoundary`.
3838

3939
This rule is intended to complement the [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react) rule set.
4040

4141
## FAQ
4242

4343
> What about `ErrorBoundary` class components? Does this lint rule support those?
4444
45-
Yes it does. [Error Boundaries](https://reactjs.org/docs/error-boundaries.html) are implemented by defining `componentDidCatch`. Because there is currently no hook equivalent, class components that implement `componentDidCatch` are allowed by default.
45+
Yes it does. [Error Boundaries](https://reactjs.org/docs/error-boundaries.html) are implemented by defining `static getDerivedStateFromError` or `componentDidCatch`. Because there is currently no hook equivalent, class components that implement `componentDidCatch` or `static getDerivedStateFromError` are allowed by default.
4646

4747
This option is configurable.
4848

@@ -102,7 +102,7 @@ module.exports = {
102102
rules: {
103103
"react-prefer-function-component/react-prefer-function-component": [
104104
"error",
105-
{ allowComponentDidCatch: false },
105+
{ allowErrorBoundary: false },
106106
],
107107
},
108108
};
@@ -142,48 +142,86 @@ const Foo = ({ foo }) => <div>{foo}</div>;
142142

143143
```js
144144
...
145-
"react-prefer-function-component": [<enabled>, { "allowComponentDidCatch": <allowComponentDidCatch>, "allowJsxUtilityClass": <allowJsxUtilityClass> }]
145+
"react-prefer-function-component": [<enabled>, { "allowErrorBoundary": <allowErrorBoundary>, "allowJsxUtilityClass": <allowJsxUtilityClass> }]
146146
...
147147
```
148148

149149
- `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
150-
- `allowComponentDidCatch`: optional boolean. set to `false` if you want to also flag class components that use `componentDidCatch` (defaults to `true`).
150+
- `allowErrorBoundary`: optional boolean. set to `false` if you want to also flag class components that use `componentDidCatch` or `static getDerivedStateFromError` (defaults to `true`).
151151
- `allowJsxUtilityClass`: optional boolean. set to `true` if you want to allow classes that contain JSX but aren't class components (defaults to `false`).
152152

153-
### `allowComponentDidCatch`
153+
### `allowErrorBoundary`
154154

155-
When `true` (the default) the rule will ignore components that use `componentDidCatch`
155+
When `true` (the default) the rule will ignore error boundary components that use `componentDidCatch` or `static getDerivedStateFromError`
156156

157157
Examples of **correct** code for this rule:
158158

159159
```jsx
160160
import { Component } from "react";
161161

162-
class Foo extends Component {
162+
class ErrorBoundary extends Component {
163163
componentDidCatch(error, errorInfo) {
164164
logErrorToMyService(error, errorInfo);
165165
}
166166

167167
render() {
168-
return <div>{this.props.foo}</div>;
168+
return <div>{this.props.children}</div>;
169+
}
170+
}
171+
```
172+
173+
```jsx
174+
import { Component } from "react";
175+
176+
class ErrorBoundary extends Component {
177+
constructor(props) {
178+
super(props);
179+
this.state = { hasError: false };
180+
}
181+
182+
static getDerivedStateFromError(error) {
183+
return { hasError: true };
184+
}
185+
186+
render() {
187+
return <div>{this.state.hasError ? "Error" : this.props.children}</div>;
169188
}
170189
}
171190
```
172191

173-
When `false` the rule will also flag components that use `componentDidCatch`
192+
When `false` the rule will also flag error boundary components
174193

175194
Examples of **incorrect** code for this rule:
176195

177196
```jsx
178197
import { Component } from "react";
179198

180-
class Foo extends Component {
199+
class ErrorBoundary extends Component {
181200
componentDidCatch(error, errorInfo) {
182201
logErrorToMyService(error, errorInfo);
183202
}
184203

185204
render() {
186-
return <div>{this.props.foo}</div>;
205+
return <div>{this.props.children}</div>;
206+
}
207+
}
208+
```
209+
210+
```jsx
211+
import { Component } from "react";
212+
213+
class ErrorBoundary extends Component {
214+
constructor(props) {
215+
super(props);
216+
this.state = { hasError: false };
217+
}
218+
219+
static getDerivedStateFromError(error) {
220+
return { hasError: true };
221+
}
222+
223+
render() {
224+
return <div>{this.state.hasError ? "Error" : this.props.children}</div>;
187225
}
188226
}
189227
```

packages/eslint-plugin-react-prefer-function-component/src/prefer-function-component/index.test.ts

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { RuleTester } from "eslint";
22
import rule, {
3-
ALLOW_COMPONENT_DID_CATCH,
3+
ALLOW_ERROR_BOUNDARY,
44
ALLOW_JSX_UTILITY_CLASS,
55
COMPONENT_SHOULD_BE_FUNCTION,
66
} from ".";
@@ -179,7 +179,7 @@ const invalidForAllOptions = [
179179
`,
180180
];
181181

182-
const componentDidCatch = [
182+
const errorBoundaries = [
183183
// Extends from Component and uses componentDidCatch
184184
`\
185185
class Foo extends React.Component {
@@ -213,6 +213,51 @@ const componentDidCatch = [
213213
}
214214
};
215215
`,
216+
// Extends from Component and uses static getDerivedStateFromError
217+
`\
218+
class Foo extends React.Component {
219+
constructor(props) {
220+
super(props);
221+
this.state = { hasError: false };
222+
}
223+
static getDerivedStateFromError(error) {
224+
return { hasError: true };
225+
}
226+
render() {
227+
return <div>{this.state.hasError ? "Error" : this.props.foo}</div>;
228+
}
229+
}
230+
`,
231+
// Extends from Component and uses static getDerivedStateFromError
232+
`\
233+
class Foo extends React.PureComponent {
234+
constructor(props) {
235+
super(props);
236+
this.state = { hasError: false };
237+
}
238+
static getDerivedStateFromError(error) {
239+
return { hasError: true };
240+
}
241+
render() {
242+
return <div>{this.state.hasError ? "Error" : this.props.foo}</div>;
243+
}
244+
}
245+
`,
246+
// Extends from Component in an expression context.
247+
`\
248+
const Foo = class extends React.Component {
249+
constructor(props) {
250+
super(props);
251+
this.state = { hasError: false };
252+
}
253+
static getDerivedStateFromError(error) {
254+
return { hasError: true };
255+
}
256+
render() {
257+
return <div>{this.state.hasError ? "Error" : this.props.foo}</div>;
258+
}
259+
};
260+
`,
216261
];
217262

218263
const jsxUtilityClass = [
@@ -230,18 +275,18 @@ ruleTester.run("prefer-function-component", rule, {
230275
...validForAllOptions.flatMap((code) => [
231276
{ code },
232277
{ code, options: [{ [ALLOW_JSX_UTILITY_CLASS]: true }] },
233-
{ code, options: [{ [ALLOW_COMPONENT_DID_CATCH]: false }] },
278+
{ code, options: [{ [ALLOW_ERROR_BOUNDARY]: false }] },
234279
{
235280
code,
236281
options: [
237282
{
238283
[ALLOW_JSX_UTILITY_CLASS]: true,
239-
[ALLOW_COMPONENT_DID_CATCH]: false,
284+
[ALLOW_ERROR_BOUNDARY]: false,
240285
},
241286
],
242287
},
243288
]),
244-
...componentDidCatch.flatMap((code) => [
289+
...errorBoundaries.flatMap((code) => [
245290
{ code },
246291
{ code, options: [{ [ALLOW_JSX_UTILITY_CLASS]: true }] },
247292
]),
@@ -261,24 +306,24 @@ ruleTester.run("prefer-function-component", rule, {
261306
{
262307
code,
263308
errors: [{ messageId: COMPONENT_SHOULD_BE_FUNCTION }],
264-
options: [{ [ALLOW_COMPONENT_DID_CATCH]: false }],
309+
options: [{ [ALLOW_ERROR_BOUNDARY]: false }],
265310
},
266311
{
267312
code,
268313
errors: [{ messageId: COMPONENT_SHOULD_BE_FUNCTION }],
269314
options: [
270315
{
271316
[ALLOW_JSX_UTILITY_CLASS]: true,
272-
[ALLOW_COMPONENT_DID_CATCH]: false,
317+
[ALLOW_ERROR_BOUNDARY]: false,
273318
},
274319
],
275320
},
276321
]),
277-
...componentDidCatch.map((code) => ({
322+
...errorBoundaries.map((code) => ({
278323
code,
279324
options: [
280325
{
281-
[ALLOW_COMPONENT_DID_CATCH]: false,
326+
[ALLOW_ERROR_BOUNDARY]: false,
282327
},
283328
],
284329
errors: [
@@ -292,7 +337,7 @@ ruleTester.run("prefer-function-component", rule, {
292337
{
293338
code,
294339
errors: [{ messageId: COMPONENT_SHOULD_BE_FUNCTION }],
295-
options: [{ [ALLOW_COMPONENT_DID_CATCH]: false }],
340+
options: [{ [ALLOW_ERROR_BOUNDARY]: false }],
296341
},
297342
]),
298343
],

packages/eslint-plugin-react-prefer-function-component/src/prefer-function-component/index.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import type { Rule } from "eslint";
55
import type { TSESTree } from "@typescript-eslint/types";
66

77
export const COMPONENT_SHOULD_BE_FUNCTION = "componentShouldBeFunction";
8-
export const ALLOW_COMPONENT_DID_CATCH = "allowComponentDidCatch";
8+
export const ALLOW_ERROR_BOUNDARY = "allowErrorBoundary";
99
export const ALLOW_JSX_UTILITY_CLASS = "allowJsxUtilityClass";
1010

1111
type ClassNode = TSESTree.ClassDeclaration | TSESTree.ClassExpression;
1212

1313
type RuleOptions = {
14-
allowComponentDidCatch?: boolean;
14+
allowErrorBoundary?: boolean;
1515
allowJsxUtilityClass?: boolean;
1616
};
1717

@@ -33,7 +33,7 @@ const rule = {
3333
{
3434
type: "object",
3535
properties: {
36-
[ALLOW_COMPONENT_DID_CATCH]: {
36+
[ALLOW_ERROR_BOUNDARY]: {
3737
default: true,
3838
type: "boolean",
3939
},
@@ -49,18 +49,22 @@ const rule = {
4949

5050
create(context: Rule.RuleContext) {
5151
const options: RuleOptions = context.options[0] ?? {};
52-
const allowComponentDidCatch = options.allowComponentDidCatch ?? true;
52+
const allowErrorBoundary = options.allowErrorBoundary ?? true;
5353
const allowJsxUtilityClass = options.allowJsxUtilityClass ?? false;
5454

5555
function shouldPreferFunction(node: ClassNode): boolean {
56-
const properties = node.body.body;
57-
const hasComponentDidCatch = properties.some((property) => {
56+
const isErrorBoundary = node.body.body.some((property) => {
5857
if ("key" in property && "name" in property.key) {
59-
return property.key.name === "componentDidCatch";
58+
return (
59+
property.key.name === "componentDidCatch" ||
60+
(property.static &&
61+
property.key.name === "getDerivedStateFromError")
62+
);
6063
}
64+
return false;
6165
});
6266

63-
if (hasComponentDidCatch && allowComponentDidCatch) {
67+
if (isErrorBoundary && allowErrorBoundary) {
6468
return false;
6569
}
6670
return true;

0 commit comments

Comments
 (0)