Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dangerous missing type validation on invalidateQueries queryKey #8684

Open
williamlmao opened this issue Feb 21, 2025 · 0 comments · May be fixed by #8691
Open

Dangerous missing type validation on invalidateQueries queryKey #8684

williamlmao opened this issue Feb 21, 2025 · 0 comments · May be fixed by #8691

Comments

@williamlmao
Copy link

Describe the bug

I just started moving code over to @TkDodo's [Query Options API pattern]](https://tkdodo.eu/blog/the-query-options-api). I noticed something was not invalidating, and it's because I forgot to .queryKey from my query options factory.

I would expect that invalidateQueries would validate that a queryKey needs to be an array. For some reason if queryKey: someValueThatIsOfTypeQueryOptions, it does not throw a type error. You can try out the example below.

Notice how this invalidateQueries is not throwing a type error here. This is problematic, because if you forget to .queryKey, the invalidation will not work.

// No error
queryClient.invalidateQueries({
  // THIS DOES NOT SHOW A TYPE ERROR ON 5.66.9
  queryKey: getTodosOptions,
});

// Should actually be
queryClient.invalidateQueries({
  // THIS DOES NOT SHOW A TYPE ERROR ON 5.66.9
  queryKey: getTodosOptions.queryKey,
});
'use client';

import { queryOptions, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

// Mock API call
const fetchTodos = async (): Promise<Todo[]> => {
  // Simulate API delay
  await new Promise((resolve) => setTimeout(resolve, 1000));

  // Add some randomness to demonstrate invalidation
  const randomSuffix = Math.floor(Math.random() * 100);

  return [
    { id: 1, title: `Learn React Query (${randomSuffix})`, completed: false },
    { id: 2, title: `Build a D&D App (${randomSuffix})`, completed: true },
    { id: 3, title: `Master TypeScript (${randomSuffix})`, completed: false },
  ];
};

const getTodosOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

export default function TestPage() {
  const [filter, setFilter] = useState<'all' | 'completed' | 'incomplete'>(
    'all'
  );
  const queryClient = useQueryClient();
  const {
    data: todos,
    isPending,
    isError,
    isRefetching,
  } = useQuery(getTodosOptions);

  if (isPending || isRefetching) return <div>Loading...</div>;
  if (isError) return <div>Error fetching todos</div>;

  return (
    <div className="p-4 text-black bg-white">
      <h1 className="text-2xl font-bold mb-4">Todo List Example</h1>

      <div className="mb-4">
        <button
          type="button"
          className="mr-2 px-3 py-1 rounded bg-green-500 text-black"
          onClick={() => {
            queryClient.invalidateQueries({
              // THIS DOES NOT SHOW A TYPE ERROR ON 5.66.9
              queryKey: getTodosOptions,
            });
          }}
        >
          Refresh Data
        </button>
        <button
          type="button"
          className={`mr-2 px-3 py-1 rounded ${filter === 'all' ? 'bg-blue-500 text-black' : 'bg-gray-200'}`}
          onClick={() => setFilter('all')}
        >
          All
        </button>
        <button
          type="button"
          className={`mr-2 px-3 py-1 rounded ${filter === 'completed' ? 'bg-blue-500 text-black' : 'bg-gray-200'}`}
          onClick={() => setFilter('completed')}
        >
          Completed
        </button>
        <button
          type="button"
          className={`px-3 py-1 rounded ${filter === 'incomplete' ? 'bg-blue-500 text-black' : 'bg-gray-200'}`}
          onClick={() => setFilter('incomplete')}
        >
          Incomplete
        </button>
      </div>

      <ul className="space-y-2">
        {todos?.map((todo) => (
          <li key={todo.id} className="flex items-center p-2 border rounded">
            <span
              className={`${todo.completed ? 'line-through text-gray-500' : ''}`}
            >
              {todo.title}
            </span>
            <span
              className={`ml-2 px-2 py-1 text-xs rounded ${todo.completed ? 'bg-green-200' : 'bg-yellow-200'}`}
            >
              {todo.completed ? 'Completed' : 'Pending'}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Your minimal, reproducible example

https://www.typescriptlang.org/play/?#code/OQVwzgpgBAxgNgSwgOwC7ANwCgsILYAOA9gE6pQDeUAjiBCQJ4DyBqCRyYANFOBAIp1GPPoPoMAwohTkAvlABmJInijAAAqgCGnbTADWAehIQtMVAFpa4zLkKlyVPgGVtqaPKUq1Js+my4aPQKZtAAKkQAJkSUWFBQCJEAXFDIIHgARvTY8WyocBApYKgkCMgA5jmwKgQF7slQGUREBTrYsjiGhlAAskQGUACCAAoAkrBacHBYMBzFihCoMAAWEdFgUAC8UFpgDMgwUAAUAJQpw8p4CJAAPGtEANoAugB8W28UcVBdUM74IHAtO4hmMoJEIICGF8tAB3LQIcjICAwqAXFTXCBHI4mMAtABuEBO7ygkFQYXwECIIFQ2IguLgBJ4AEYAAxsk4nALxH6DSKREkqaAkHTRPBIsAbVAxcF4OYlIHQMp4yaJIHsZBfWa6KDC5Ci5wgBQKBAADy2vSBywAdAo4M0SEceparbrRacoAAqKCslmcnDxEyoEAkZBQB5feJURIpJk8PIFFIAAwAMqYQ1AAEqmcxQMSMY4AEgorpUBqNptkJ0TPFmhDqEAaITgkCgsi4EcoCQaACY4wiE1BEwAhEAIOD8rRQAAiADIp0MCARC8WRaXDcaTZXq9U64sGykSnRW+34pGuykAMx9-KFQdO4r0KBhBgECDOGClVjLkt4Msbrc1jU9aNpMLZtl8TztAEWrzOUiz3GALBsHM5rWIwSHqmARyfPEaEMAA0hADApA8wBSuswBPCeNBCAwABiyApAoiwrAh7aVgEEAmsQZBghAIQAuQCggAcyGhmEdKoMMWhwe6OHVNqDzGnA7gkDwpJ0WOqlPOaLhuBANzAJMcDAFAAA+ai1rUe6RKZFnAGUVn1sALxHB2RlTMAXx+vEMHkHhUhIGgumQHmkjSGgpxVH5sSnmCQJaAeURENwHbXMMKCRGU5TUfE1wAKIkMoalpWAWbMUsyzZdR8jbKItFHHBZLJYhrCYX6XwIAoxzpZl2XmRZ1zlSxVUVESgbBqGNxZXiLzJkQWhZRUVorTchgzS8VRdT1YCFcV42LJNUDTQgs17aQCyVf15EpWtG1cjqh3pm5cUnXisCAhKAByWh4BAmwAEQEBYAAsUDuCalgZICAwZOUFgwlV7gAy8HbxDcyxMh9uxgD9f2AxDljdiacCKBwUMtPymSgyj9xQMm1zkPlJq-dZa2Y6jaPHTN2Pfb9-0A9TIMo1z6MZNSUoanF0vgy+Avi6gksA6Lp7wDjeMC3gJAWN2UAECaFgXnrDAWFjygieC-JwxY5QmCgFgAKxsuDXFQzD+jKzLcUcIFBibBQ7qbB8KtxQFEWoFaSoqpECp5kgWHyV70s-GEAASozONOTD5ZnX1MGEvyp0wADqQxPgAmsM+VQPlGYZkwGZQEwX1QA7VoAGzt1aACcIfS3hhHEVATUIRhcy5UnrY+ZPsgdEnqOT5m-E4ss04JSra0K5LC9ezcW8cH3qBy4D+-IJ7k9q3z+MUImWs63rBtGwQJtm1SeoNlARbKapWybNsHkmSgAAfjUNbaGdBHbO0JhYaGZh9CmRSMAa2tstAm27GyYAshExz0nj7RAfsA5EiDiSRYmkVL0COAA4AJwcEyx3knQYUwN6GFPvQ6We8JYH0XkfV8J9OFnz7pfXG-N-a321rrfWhtjamx1G-S2n8KDf0fH-f+TkbKmRAUg+G4CICQJZC7SGMD3YINAfDFBaCMFYNoV7PBCACGBzeBpLSFDgBqPqNQ6xcU2EywkEBGyzDWEb1PofY+ANT7nyTkIjWojJFPxfrIi2H8v7OJIL-f+jk-HuA0aYoxECnb6OgbAgwJitE22FBYlkmDsF91sfYohjjSEpMoRk3cWSaEq28dLUYBxMkQACfwzpd1TqcxljcAEvNhH4wBmAAgoQLBoJFknCgN0wBAKtHgLQBAsQ3XqccPuNxEBQH0ERf2N1I6RHkFEkRANbRcQSO4PAYALAwBkI+YGusmgkHBKk8279IiLMXsdGZOg+5xSudfRMRYzluI-poxASILCoGWObcoq9oHmL0SUqpnivadKWWc+MEAcWjMMMC5AeL2FktBarT6kz-o3zwHAe+kiJHxOgSaDYvz5FQuSlaGF-JNHILtsgHWGCoCIOtgwCEdoYSisqVY6lFKZbLN5fy4BahfGtIbCUjKepsqYOpWtMlSrjqGEQBSjkOK1oAjYUM2a3l2hYCAA

Steps to reproduce

Paste the code above into a react app.

Expected behavior

invalidateQueries throws type error when queryKey is not of type array

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

macOS Sequoia

Tanstack Query adapter

None

TanStack Query version

5.66.9

TypeScript version

5.5.4

Additional context

No response

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
1 participant