Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/violet-falcons-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-react-server-components": minor
---

Error on class components
39 changes: 39 additions & 0 deletions src/rules/use-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,45 @@ function App() {
});
});

describe("CLASS COMPONENTS", () => {
ruleTester.run("class components", rule, {
valid: [
{
code: `'use client';

import React, {Component} from 'react';

class Foo extends Component {
render() {
return <div />
}
}`,
},
],
invalid: [
{
code: `import React, {Component} from 'react';

class Foo extends Component {
render() {
return <div />
}
}`,
errors: [{ messageId: "addUseClientClassComponent" }],
output: `'use client';

import React, {Component} from 'react';

class Foo extends Component {
render() {
return <div />
}
}`,
},
],
});
});

describe("behaviors", () => {
describe("comments at the top of the file", () => {
ruleTester.run("comments", rule, {
Expand Down
71 changes: 38 additions & 33 deletions src/rules/use-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
Expression,
ExpressionStatement,
Identifier,
ImportSpecifier,
Node,
Program,
SpreadElement,
Expand All @@ -12,6 +13,8 @@ import { reactEvents } from "./react-events";
import { JSXOpeningElement } from "estree-jsx";
// @ts-expect-error
import Components from "eslint-plugin-react/lib/util/Components";
// @ts-expect-error
import componentUtil from "eslint-plugin-react/lib/util/componentUtil";

const HOOK_REGEX = /^use[A-Z]/;
const useClientRegex = /^('|")use client('|")/;
Expand Down Expand Up @@ -51,6 +54,8 @@ const meta: Rule.RuleModule["meta"] = {
'Browser APIs only work in Client Components. Add the "use client" directive at the top of the file to use it.',
addUseClientCallbacks:
'Functions can only be passed as props to Client Components. Add the "use client" directive at the top of the file to use it.',
addUseClientClassComponent:
'React Class Components can only be used in Client Components. Add the "use client" directive at the top of the file.',
removeUseClient:
"This file does not require the 'use client' directive, and it should be removed.",
},
Expand Down Expand Up @@ -115,7 +120,7 @@ const create = Components.detect(

parentNode = node;
const scope = context.getScope();
// Report variables not declared at all
// Collect undeclared variables (ie, used global variables)
scope.through.forEach((reference) => {
undeclaredReferences.add(reference.identifier.name);
});
Expand All @@ -125,8 +130,8 @@ const create = Components.detect(
if (node.source.value === "react") {
node.specifiers
.filter((spec) => spec.type === "ImportSpecifier")
.forEach((spec) => {
// @ts-expect-error
.forEach((spac: any) => {
const spec = spac as ImportSpecifier;
reactImports[spec.local.name] = spec.imported.name;
});
const namespace = node.specifiers.find(
Expand All @@ -150,37 +155,31 @@ const create = Components.detect(
reportMissingDirective("addUseClientBrowserAPI", node);
}
},
VariableDeclaration(node) {
// Catch using hooks within a component
const declarator = node.declarations[0];

if (declarator.init && declarator.init.type === "CallExpression") {
const expression = declarator.init;
let name = "";
if (
expression.callee.type === "Identifier" &&
"name" in expression.callee
) {
name = expression.callee.name;
} else if (
expression.callee.type === "MemberExpression" &&
"name" in expression.callee.property
) {
name = expression.callee.property.name;
}
CallExpression(expression) {
let name = "";
if (
expression.callee.type === "Identifier" &&
"name" in expression.callee
) {
name = expression.callee.name;
} else if (
expression.callee.type === "MemberExpression" &&
"name" in expression.callee.property
) {
name = expression.callee.property.name;
}

if (
HOOK_REGEX.test(name) &&
// Is in a function...
context.getScope().type === "function" &&
// But only if that function is a component
Boolean(util.getParentComponent(node))
) {
instances.push(name);
reportMissingDirective("addUseClientHooks", expression.callee, {
hook: name,
});
}
if (
HOOK_REGEX.test(name) &&
// Is in a function...
context.getScope().type === "function" &&
// But only if that function is a component
Boolean(util.getParentComponent(expression))
) {
instances.push(name);
reportMissingDirective("addUseClientHooks", expression.callee, {
hook: name,
});
}
},
MemberExpression(node) {
Expand Down Expand Up @@ -266,6 +265,12 @@ const create = Components.detect(
}
}
},
ClassDeclaration(node) {
if (componentUtil.isES6Component(node, context)) {
instances.push(node.id?.name);
reportMissingDirective("addUseClientClassComponent", node);
}
},

"ExpressionStatement:exit"(
node: ExpressionStatement & Rule.NodeParentExtension
Expand Down