This error occurs when a non-array value is passed as a queryKey to React Query/TanStack Query hooks. Since v4, queryKey must always be an array, even for simple string keys.
This error indicates that React Query received a queryKey that is not an array. Starting with React Query v4, query keys must be arrays at the top level to ensure type consistency across the library. In earlier versions (v3), query keys could be either strings or arrays. However, this flexibility caused type inconsistencies, especially when using features like predicate filters or accessing query.queryKey in global query functions. To resolve this, the library standardized on array-only keys. When the queryKey is passed to QueryFunctionContext, it's always an array, which makes it easier to work with Array<unknown> on the type level. This consistency is crucial for features like invalidateQueries, getQueryData, and other query management APIs.
Convert any string-based queryKey to an array format:
// ❌ Wrong - string key
useQuery({
queryKey: 'todos',
queryFn: fetchTodos,
})
// ✅ Correct - array key
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})This is the most common fix and works for simple queries.
When your query depends on variables, include them in the array:
// Single parameter
useQuery({
queryKey: ['todo', todoId],
queryFn: () => fetchTodo(todoId),
})
// Multiple parameters
useQuery({
queryKey: ['todos', { status, page }],
queryFn: () => fetchTodos({ status, page }),
})
// Nested arrays are allowed
useQuery({
queryKey: ['projects', projectId, 'tasks', { completed: true }],
queryFn: () => fetchProjectTasks(projectId, { completed: true }),
})Order matters - different orderings of the same items create different keys.
Ensure all query client methods also use array keys:
// ❌ Wrong
queryClient.invalidateQueries('todos')
queryClient.getQueryData('todos')
queryClient.setQueryData('todos', newData)
// ✅ Correct
queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], newData)This applies to invalidateQueries, getQueryData, setQueryData, removeQueries, and other query client methods.
Create a centralized key factory to avoid mistakes:
// Define your key factory
const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
}
// Use in queries
useQuery({
queryKey: todoKeys.detail(todoId),
queryFn: () => fetchTodo(todoId),
})
// Use in invalidation
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })This pattern ensures type safety and consistency across your application.
If migrating from v3 to v4+, use the official codemod:
# Install jscodeshift if needed
npm install -g jscodeshift
# Run the codemod (adjust paths as needed)
npx jscodeshift ./src --transform ./node_modules/@tanstack/react-query-codemods/dist/v4/key-transformation.jsAfter running the codemod, format your code:
npm run prettier -- --write .
npm run eslint -- --fix .Review the changes carefully as codemods may not catch all edge cases.
Why Arrays Are Required:
React Query's decision to enforce array-only keys was driven by type safety. When keys could be either strings or arrays, the type system had to handle both possibilities (string | Array<unknown>), making it difficult to write type-safe predicate functions and other advanced features. By standardizing on arrays, the library can guarantee consistent types across all APIs.
Query Key Serialization:
React Query serializes query keys to create unique cache entries. As long as the key is serializable (no functions, circular references, etc.), you can use nested objects and arrays. The library uses a deterministic hashing algorithm to convert keys into cache identifiers.
Performance Considerations:
While wrapping a single string in an array adds minimal overhead, the consistency benefits outweigh any marginal performance cost. The library is highly optimized for array-based key lookups.
ESLint Rules:
Consider using the @tanstack/eslint-plugin-query package, which includes rules like "exhaustive-deps" that help catch queryKey mistakes during development. This catches issues like missing dependencies in your query keys before runtime.
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite