/**
* Tests for: prefer-zustand-state-management
*
* Detects excessive use of React state hooks (useState, useReducer, useContext)
* and suggests using Zustand stores for better state management.
*/
import { RuleTester } from "@typescript-eslint/rule-tester";
import { describe, it, afterAll } from "vitest";
import rule from "./prefer-zustand-state-management";
RuleTester.afterAll = afterAll;
RuleTester.describe = describe;
RuleTester.it = it;
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
});
ruleTester.run("prefer-zustand-state-management", rule, {
valid: [
// ============================================
// NO STATE HOOKS
// ============================================
{
name: "component with no hooks",
code: `
function MyComponent() {
return
Hello
;
}
`,
},
{
name: "component with only non-state hooks",
code: `
function MyComponent() {
useEffect(() => {}, []);
useMemo(() => computed, [dep]);
useCallback(() => {}, []);
useRef(null);
return Hello
;
}
`,
},
{
name: "arrow function component with no state hooks",
code: `
const MyComponent = () => {
useEffect(() => {}, []);
return Hello
;
};
`,
},
// ============================================
// UNDER DEFAULT THRESHOLD (3)
// ============================================
{
name: "single useState is fine",
code: `
function MyComponent() {
const [count, setCount] = useState(0);
return {count}
;
}
`,
},
{
name: "two useState calls is fine",
code: `
function MyComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
return {count} {name}
;
}
`,
},
{
name: "three useState calls is at threshold (valid)",
code: `
function MyComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
const [active, setActive] = useState(false);
return {count}
;
}
`,
},
{
name: "single useReducer is fine",
code: `
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
return {state.value}
;
}
`,
},
{
name: "single useContext is fine",
code: `
function MyComponent() {
const theme = useContext(ThemeContext);
return ;
}
`,
},
{
name: "mixed hooks under threshold",
code: `
function MyComponent() {
const [count, setCount] = useState(0);
const theme = useContext(ThemeContext);
const [state, dispatch] = useReducer(reducer, init);
return {count}
;
}
`,
},
// ============================================
// CUSTOM THRESHOLD CONFIGURATION
// ============================================
{
name: "five useState with higher threshold (5)",
code: `
function MyComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
const [e, setE] = useState(0);
return {a}
;
}
`,
options: [{ maxStateHooks: 5 }],
},
{
name: "ten useState with threshold of 10",
code: `
function MyComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
const [e, setE] = useState(0);
const [f, setF] = useState(0);
const [g, setG] = useState(0);
const [h, setH] = useState(0);
const [i, setI] = useState(0);
const [j, setJ] = useState(0);
return {a}
;
}
`,
options: [{ maxStateHooks: 10 }],
},
// ============================================
// SELECTIVE HOOK COUNTING
// ============================================
{
name: "many useState but useState counting disabled",
code: `
function MyComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
const [e, setE] = useState(0);
return {a}
;
}
`,
options: [{ countUseState: false }],
},
{
name: "many useReducer but useReducer counting disabled",
code: `
function MyComponent() {
const [s1, d1] = useReducer(r, i);
const [s2, d2] = useReducer(r, i);
const [s3, d3] = useReducer(r, i);
const [s4, d4] = useReducer(r, i);
return {s1}
;
}
`,
options: [{ countUseReducer: false }],
},
{
name: "many useContext but useContext counting disabled",
code: `
function MyComponent() {
const a = useContext(A);
const b = useContext(B);
const c = useContext(C);
const d = useContext(D);
const e = useContext(E);
return {a}
;
}
`,
options: [{ countUseContext: false }],
},
{
name: "only count useState when others disabled",
code: `
function MyComponent() {
const [count, setCount] = useState(0);
const [s1, d1] = useReducer(r, i);
const [s2, d2] = useReducer(r, i);
const a = useContext(A);
const b = useContext(B);
return {count}
;
}
`,
options: [{ countUseReducer: false, countUseContext: false }],
},
// ============================================
// MULTIPLE COMPONENTS (each counted separately)
// ============================================
{
name: "multiple components each under threshold",
code: `
function ComponentA() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
return {a}
;
}
function ComponentB() {
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return {c}
;
}
`,
},
{
name: "arrow and function components each under threshold",
code: `
const ComponentA = () => {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
return {a}
;
};
function ComponentB() {
const [c, setC] = useState(0);
return {c}
;
}
`,
},
// ============================================
// CUSTOM HOOKS (not components - should be ignored)
// ============================================
{
name: "custom hook with many useState (hooks can have more state)",
code: `
function useFormState() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [address, setAddress] = useState("");
const [city, setCity] = useState("");
return { name, email, phone, address, city };
}
`,
},
{
name: "custom hook starting with use",
code: `
const useComplexState = () => {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return { a, b, c, d };
};
`,
},
// ============================================
// REACT.useState CALLS
// ============================================
{
name: "React.useState under threshold",
code: `
function MyComponent() {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState("");
return {count}
;
}
`,
},
// ============================================
// NON-COMPONENT FUNCTIONS (should be ignored)
// ============================================
{
name: "regular function (lowercase, no JSX) is ignored",
code: `
function processData() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return { a, b, c, d };
}
`,
},
// ============================================
// NESTED FUNCTIONS (hooks in nested shouldn't count for parent)
// ============================================
{
name: "hooks in event handlers don't count (though invalid React)",
code: `
function MyComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
// These shouldn't be here in real code, but if they are,
// they shouldn't count toward the component's total
};
return {count}
;
}
`,
},
// ============================================
// EDGE CASES
// ============================================
{
name: "empty function",
code: `
function MyComponent() {}
`,
},
{
name: "component returning null",
code: `
function MyComponent() {
const [show, setShow] = useState(false);
return null;
}
`,
},
{
name: "forwardRef component under threshold",
code: `
const MyComponent = forwardRef(function MyComponent(props, ref) {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
return {count}
;
});
`,
},
{
name: "memo component under threshold",
code: `
const MyComponent = memo(function MyComponent() {
const [count, setCount] = useState(0);
return {count}
;
});
`,
},
],
invalid: [
// ============================================
// EXCEEDING DEFAULT THRESHOLD (3)
// ============================================
{
name: "four useState calls exceeds threshold",
code: `
function MyComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return {a}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
{
name: "five useState calls",
code: `
function MyComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
const [e, setE] = useState(0);
return {a}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 5, max: 3, component: "MyComponent" },
},
],
},
{
name: "four useReducer calls exceeds threshold",
code: `
function MyComponent() {
const [s1, d1] = useReducer(r, i);
const [s2, d2] = useReducer(r, i);
const [s3, d3] = useReducer(r, i);
const [s4, d4] = useReducer(r, i);
return {s1}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
{
name: "four useContext calls exceeds threshold",
code: `
function MyComponent() {
const a = useContext(A);
const b = useContext(B);
const c = useContext(C);
const d = useContext(D);
return {a}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
{
name: "mixed hooks exceeding threshold",
code: `
function MyComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
const [state, dispatch] = useReducer(reducer, init);
const theme = useContext(ThemeContext);
return {count}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
// ============================================
// ARROW FUNCTION COMPONENTS
// ============================================
{
name: "arrow function component exceeds threshold",
code: `
const MyComponent = () => {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return {a}
;
};
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
{
name: "arrow function with implicit return exceeds threshold",
code: `
const MyComponent = () => {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return {a}
;
};
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
// ============================================
// CUSTOM THRESHOLD
// ============================================
{
name: "exceeds custom threshold of 2",
code: `
function MyComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
return {a}
;
}
`,
options: [{ maxStateHooks: 2 }],
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 3, max: 2, component: "MyComponent" },
},
],
},
{
name: "exceeds custom threshold of 1",
code: `
function MyComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
return {a}
;
}
`,
options: [{ maxStateHooks: 1 }],
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 2, max: 1, component: "MyComponent" },
},
],
},
// ============================================
// SELECTIVE COUNTING STILL EXCEEDS
// ============================================
{
name: "useState only counting still exceeds",
code: `
function MyComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
const theme = useContext(ThemeContext);
return {a}
;
}
`,
options: [{ countUseContext: false }],
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
// ============================================
// MULTIPLE COMPONENTS (one exceeds)
// ============================================
{
name: "one of multiple components exceeds threshold",
code: `
function ComponentA() {
const [a, setA] = useState(0);
return {a}
;
}
function ComponentB() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return {a}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "ComponentB" },
},
],
},
{
name: "multiple components both exceed threshold",
code: `
function ComponentA() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return {a}
;
}
function ComponentB() {
const [e, setE] = useState(0);
const [f, setF] = useState(0);
const [g, setG] = useState(0);
const [h, setH] = useState(0);
const [i, setI] = useState(0);
return {e}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "ComponentA" },
},
{
messageId: "excessiveStateHooks",
data: { count: 5, max: 3, component: "ComponentB" },
},
],
},
// ============================================
// REACT.useState / REACT.useReducer / REACT.useContext
// ============================================
{
name: "React.useState exceeds threshold",
code: `
function MyComponent() {
const [a, setA] = React.useState(0);
const [b, setB] = React.useState(0);
const [c, setC] = React.useState(0);
const [d, setD] = React.useState(0);
return {a}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
{
name: "mixed React.* and direct hook calls",
code: `
function MyComponent() {
const [a, setA] = React.useState(0);
const [b, setB] = useState(0);
const [c, setC] = React.useState(0);
const [d, setD] = useState(0);
return {a}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
// ============================================
// FORWARDREF / MEMO WRAPPED COMPONENTS
// ============================================
{
name: "forwardRef component exceeds threshold",
code: `
const MyComponent = forwardRef(function MyComponent(props, ref) {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return {a}
;
});
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
{
name: "memo component exceeds threshold",
code: `
const MyComponent = memo(function MyComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return {a}
;
});
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
{
name: "forwardRef with arrow function exceeds threshold",
code: `
const MyComponent = forwardRef((props, ref) => {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return {a}
;
});
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
// ============================================
// ANONYMOUS COMPONENTS
// ============================================
{
name: "anonymous arrow function component exceeds threshold",
code: `
export default () => {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
return {a}
;
};
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "AnonymousComponent" },
},
],
},
// ============================================
// REAL-WORLD PATTERNS
// ============================================
{
name: "form component with too many state fields",
code: `
function ContactForm() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [message, setMessage] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
return (
);
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 6, max: 3, component: "ContactForm" },
},
],
},
{
name: "dashboard component with too much local state",
code: `
function Dashboard() {
const [user, setUser] = useState(null);
const [projects, setProjects] = useState([]);
const [notifications, setNotifications] = useState([]);
const [settings, setSettings] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const theme = useContext(ThemeContext);
const auth = useContext(AuthContext);
return Dashboard
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 8, max: 3, component: "Dashboard" },
},
],
},
// ============================================
// EDGE CASES
// ============================================
{
name: "useState with lazy initializer exceeds threshold",
code: `
function MyComponent() {
const [a, setA] = useState(() => expensiveComputation());
const [b, setB] = useState(() => anotherComputation());
const [c, setC] = useState(() => yetAnother());
const [d, setD] = useState(() => oneMore());
return {a}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
{
name: "useReducer with lazy init exceeds threshold",
code: `
function MyComponent() {
const [s1, d1] = useReducer(r, arg, init);
const [s2, d2] = useReducer(r, arg, init);
const [s3, d3] = useReducer(r, arg, init);
const [s4, d4] = useReducer(r, arg, init);
return {s1}
;
}
`,
errors: [
{
messageId: "excessiveStateHooks",
data: { count: 4, max: 3, component: "MyComponent" },
},
],
},
],
});