/**
* Tests for: prefer-store-selectors
*
* Tests the detection of useMemo patterns that should be refactored to
* Zustand store selectors for better performance and code organization.
*/
import { RuleTester } from "@typescript-eslint/rule-tester";
import { describe, it, afterAll } from "vitest";
import rule from "./prefer-store-selectors.js";
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-store-selectors", rule, {
valid: [
// ============================================
// USING SELECTORS DIRECTLY
// ============================================
{
name: "using selector directly - named function",
code: `const items = useStore(selectFilteredItems);`,
},
{
name: "using selector directly - inline arrow",
code: `const count = useStore((s) => s.count);`,
},
{
name: "using selector with computation inside",
code: `const activeItems = useStore((s) => s.items.filter(i => i.active));`,
},
{
name: "using selector from object",
code: `const data = useDataStore(selectors.getData);`,
},
// ============================================
// USEMEMO WITH LOCAL STATE ONLY
// ============================================
{
name: "useMemo with useState - local state only",
code: `
const [count, setCount] = useState(0);
const doubled = useMemo(() => count * 2, [count]);
`,
},
{
name: "useMemo with multiple local state values",
code: `
const [items, setItems] = useState([]);
const [filter, setFilter] = useState('');
const filtered = useMemo(() => items.filter(i => i.name.includes(filter)), [items, filter]);
`,
},
{
name: "useMemo with local state array methods",
code: `
const [numbers, setNumbers] = useState([1, 2, 3]);
const sum = useMemo(() => numbers.reduce((a, b) => a + b, 0), [numbers]);
`,
},
// ============================================
// USEMEMO WITH PROPS
// ============================================
{
name: "useMemo with props - sorting",
code: `
function Component(props) {
const sorted = useMemo(() => props.items.sort((a, b) => a.name.localeCompare(b.name)), [props.items]);
return
;
}
`,
},
{
name: "useMemo with props - filtering",
code: `
function Component({ data }) {
const active = useMemo(() => data.filter(d => d.isActive), [data]);
return
;
}
`,
},
{
name: "useMemo with props - mapping",
code: `
const Component = ({ users }) => {
const names = useMemo(() => users.map(u => u.name), [users]);
return
;
};
`,
},
// ============================================
// STORE DATA USED DIRECTLY (NO USEMEMO)
// ============================================
{
name: "store data used directly without transformation",
code: `
const items = useItemStore((s) => s.items);
return
;
`,
},
{
name: "store data passed to component directly",
code: `
const count = useCounterStore((s) => s.count);
const increment = useCounterStore((s) => s.increment);
return ;
`,
},
// ============================================
// USEMEMO NOT DEPENDING ON STORE VARIABLES
// ============================================
{
name: "useMemo with constants only",
code: `
const items = useStore((s) => s.items);
const config = useMemo(() => ({ pageSize: 10, sortOrder: 'asc' }), []);
`,
},
{
name: "useMemo with external variable not from store",
code: `
const items = useStore((s) => s.items);
const externalData = fetchedData;
const processed = useMemo(() => externalData.map(d => d.value), [externalData]);
`,
},
{
name: "useMemo with ref dependency",
code: `
const items = useStore((s) => s.items);
const ref = useRef([]);
const cached = useMemo(() => ref.current.slice(), [ref.current]);
`,
},
// ============================================
// NON-TRANSFORMATION USEMEMO
// ============================================
{
name: "useMemo creating object from store value",
code: `
const count = useStore((s) => s.count);
const obj = useMemo(() => ({ current: count }), [count]);
`,
},
{
name: "useMemo with simple computation (no array methods)",
code: `
const total = useStore((s) => s.total);
const tax = useMemo(() => total * 0.1, [total]);
`,
},
// ============================================
// CUSTOM STORE PATTERN
// ============================================
{
name: "custom pattern - non-matching hook with useMemo",
code: `
const items = useData();
const filtered = useMemo(() => items.filter(i => i.active), [items]);
`,
options: [{ storeHookPattern: "^use.*Store$" }],
},
// ============================================
// EDGE CASES
// ============================================
{
name: "empty useMemo",
code: `
const items = useStore((s) => s.items);
const empty = useMemo(() => [], []);
`,
},
{
name: "useMemo with unrelated array literal",
code: `
const items = useStore((s) => s.items);
const defaults = useMemo(() => [1, 2, 3].filter(x => x > 1), []);
`,
},
{
name: "useMemo in different scope than store",
code: `
function Parent() {
const items = useStore((s) => s.items);
return ;
}
function Child({ items }) {
const filtered = useMemo(() => items.filter(i => i.active), [items]);
return
;
}
`,
},
],
invalid: [
// ============================================
// SINGLE USEMEMO WITH STORE DATA
// ============================================
{
name: "useMemo filtering store data",
code: `
const items = useComposedStore((s) => s.items);
const filtered = useMemo(() => items.filter(x => x.active), [items]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
],
},
{
name: "useMemo mapping store data",
code: `
const users = useUserStore((s) => s.users);
const names = useMemo(() => users.map(u => u.name), [users]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "users" },
},
],
},
{
name: "useMemo reducing store data",
code: `
const items = useCartStore((s) => s.items);
const total = useMemo(() => items.reduce((sum, i) => sum + i.price, 0), [items]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
],
},
{
name: "useMemo sorting store data",
code: `
const products = useProductStore((s) => s.products);
const sorted = useMemo(() => products.sort((a, b) => a.price - b.price), [products]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "products" },
},
],
},
{
name: "useMemo with find on store data",
code: `
const users = useUserStore((s) => s.users);
const admin = useMemo(() => users.find(u => u.role === 'admin'), [users]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "users" },
},
],
},
{
name: "useMemo with some on store data",
code: `
const tasks = useTaskStore((s) => s.tasks);
const hasCompleted = useMemo(() => tasks.some(t => t.completed), [tasks]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "tasks" },
},
],
},
{
name: "useMemo with every on store data",
code: `
const items = useItemStore((s) => s.items);
const allActive = useMemo(() => items.every(i => i.isActive), [items]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
],
},
{
name: "useMemo with flat on store data",
code: `
const nested = useDataStore((s) => s.nested);
const flattened = useMemo(() => nested.flat(), [nested]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "nested" },
},
],
},
{
name: "useMemo with flatMap on store data",
code: `
const groups = useGroupStore((s) => s.groups);
const allItems = useMemo(() => groups.flatMap(g => g.items), [groups]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "groups" },
},
],
},
{
name: "useMemo with slice on store data",
code: `
const items = useItemStore((s) => s.items);
const first10 = useMemo(() => items.slice(0, 10), [items]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
],
},
{
name: "useMemo with concat on store data",
code: `
const items = useItemStore((s) => s.items);
const extended = useMemo(() => items.concat([{ id: 'new' }]), [items]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
],
},
{
name: "useMemo with join on store data",
code: `
const tags = useTagStore((s) => s.tags);
const tagString = useMemo(() => tags.join(', '), [tags]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "tags" },
},
],
},
// ============================================
// ARRAY TRANSFORMATION FUNCTIONS
// ============================================
{
name: "useMemo with Array.from on store data",
code: `
const itemSet = useStore((s) => s.itemSet);
const itemArray = useMemo(() => Array.from(itemSet), [itemSet]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "itemSet" },
},
],
},
{
name: "useMemo with Object.keys on store data",
code: `
const data = useDataStore((s) => s.data);
const keys = useMemo(() => Object.keys(data), [data]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "data" },
},
],
},
{
name: "useMemo with Object.values on store data",
code: `
const dataMap = useStore((s) => s.dataMap);
const values = useMemo(() => Object.values(dataMap), [dataMap]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "dataMap" },
},
],
},
{
name: "useMemo with Object.entries on store data",
code: `
const config = useConfigStore((s) => s.config);
const entries = useMemo(() => Object.entries(config), [config]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "config" },
},
],
},
// ============================================
// CHAINED DERIVATIONS FROM STORE
// ============================================
{
name: "chained useMemo - filter then map",
code: `
const items = useItemStore((s) => s.items);
const active = useMemo(() => items.filter(i => i.isActive), [items]);
const names = useMemo(() => active.map(i => i.name), [active]);
`,
errors: [
{
messageId: "chainedDerivedState",
},
],
},
{
name: "chained useMemo - complex dashboard example",
code: `
const issues = useComposedStore((s) => s.issues);
const all = useMemo(() => issues.flat(), [issues]);
const counts = useMemo(() => all.filter(i => i.severity === 'error'), [all]);
`,
errors: [
{
messageId: "chainedDerivedState",
},
],
},
{
name: "chained useMemo - filter, sort, then slice",
code: `
const data = useDataStore((s) => s.data);
const filtered = useMemo(() => data.filter(d => d.valid), [data]);
const sorted = useMemo(() => filtered.sort((a, b) => b.date - a.date), [filtered]);
const top10 = useMemo(() => sorted.slice(0, 10), [sorted]);
`,
errors: [
{
messageId: "chainedDerivedState",
},
],
},
{
name: "chained useMemo - Set to array then filter",
code: `
const itemsSet = useStore((s) => s.itemsSet);
const itemsArray = useMemo(() => Array.from(itemsSet), [itemsSet]);
const activeItems = useMemo(() => itemsArray.filter(i => i.active), [itemsArray]);
`,
errors: [
{
messageId: "chainedDerivedState",
},
],
},
// ============================================
// MULTIPLE INDEPENDENT USEMEMO FROM STORE
// ============================================
{
name: "multiple independent useMemo from same store variable",
code: `
const items = useItemStore((s) => s.items);
const active = useMemo(() => items.filter(i => i.active), [items]);
const total = useMemo(() => items.reduce((sum, i) => sum + i.price, 0), [items]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
],
},
{
name: "multiple useMemo from different store variables",
code: `
const users = useUserStore((s) => s.users);
const products = useProductStore((s) => s.products);
const adminUsers = useMemo(() => users.filter(u => u.role === 'admin'), [users]);
const activeProducts = useMemo(() => products.filter(p => p.isActive), [products]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "users" },
},
{
messageId: "useMemoWithStoreData",
data: { varName: "products" },
},
],
},
// ============================================
// BLOCK BODY USEMEMO
// ============================================
{
name: "useMemo with block body returning filtered data",
code: `
const items = useStore((s) => s.items);
const filtered = useMemo(() => {
return items.filter(i => i.active);
}, [items]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
],
},
{
name: "useMemo with block body and intermediate variable",
code: `
const data = useDataStore((s) => s.data);
const processed = useMemo(() => {
const filtered = data.filter(d => d.valid);
return filtered;
}, [data]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "data" },
},
],
},
// ============================================
// IN COMPONENT CONTEXT
// ============================================
{
name: "useMemo in function component",
code: `
function ProductList() {
const products = useProductStore((s) => s.products);
const activeProducts = useMemo(
() => products.filter((p) => p.isActive),
[products]
);
return
;
}
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "products" },
},
],
},
{
name: "useMemo in arrow function component",
code: `
const TaskList = () => {
const tasks = useTaskStore((s) => s.tasks);
const completedTasks = useMemo(() => tasks.filter(t => t.done), [tasks]);
return {completedTasks.map(t => - {t.title}
)}
;
};
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "tasks" },
},
],
},
// ============================================
// CUSTOM STORE PATTERN
// ============================================
{
name: "custom store pattern - useGlobalStore",
code: `
const items = useGlobalStore((s) => s.items);
const filtered = useMemo(() => items.filter(i => i.visible), [items]);
`,
options: [{ storeHookPattern: "^useGlobal.*$" }],
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
],
},
{
name: "custom store pattern - useAppData",
code: `
const users = useAppData((s) => s.users);
const activeUsers = useMemo(() => users.filter(u => u.active), [users]);
`,
options: [{ storeHookPattern: "^useApp.*$" }],
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "users" },
},
],
},
// ============================================
// CHAINED METHOD CALLS
// ============================================
{
name: "chained methods in single useMemo - filter then map",
code: `
const items = useItemStore((s) => s.items);
const result = useMemo(() => items.filter(i => i.active).map(i => i.name), [items]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
],
},
{
name: "chained methods - filter, sort, slice",
code: `
const products = useStore((s) => s.products);
const topProducts = useMemo(
() => products.filter(p => p.rating > 4).sort((a, b) => b.sales - a.sales).slice(0, 5),
[products]
);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "products" },
},
],
},
// ============================================
// EDGE CASES
// ============================================
{
name: "useMemo with includes check on store array",
code: `
const favorites = useFavoritesStore((s) => s.favorites);
const isFavorite = useMemo(() => favorites.includes(currentId), [favorites, currentId]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "favorites" },
},
],
},
{
name: "useMemo with findIndex on store data",
code: `
const items = useItemStore((s) => s.items);
const index = useMemo(() => items.findIndex(i => i.id === selectedId), [items, selectedId]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "items" },
},
],
},
{
name: "useMemo with reverse on store data",
code: `
const messages = useMessageStore((s) => s.messages);
const reversed = useMemo(() => messages.reverse(), [messages]);
`,
errors: [
{
messageId: "useMemoWithStoreData",
data: { varName: "messages" },
},
],
},
],
});