CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-reduxjs--toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

Pending
Overview
Eval results
Files

rtk-query-react.mddocs/

RTK Query React

RTK Query React provides React-specific hooks and components for seamless integration with RTK Query data fetching. Available from @reduxjs/toolkit/query/react.

Capabilities

Auto-generated Hooks

RTK Query automatically generates React hooks for each endpoint, providing type-safe data fetching and state management.

/**
 * Auto-generated query hook for data fetching
 * Generated for each query endpoint as `use{EndpointName}Query`
 */
type UseQueryHook<ResultType, QueryArg> = (
  arg: QueryArg,
  options?: UseQueryOptions<QueryArg>
) => UseQueryResult<ResultType>;

interface UseQueryOptions<QueryArg> {
  /** Skip query execution */
  skip?: boolean;
  /** Polling interval in milliseconds */
  pollingInterval?: number;
  /** Skip polling when window is unfocused */
  skipPollingIfUnfocused?: boolean;
  /** Refetch on mount or arg change */
  refetchOnMountOrArgChange?: boolean | number;
  /** Refetch on window focus */
  refetchOnFocus?: boolean;
  /** Refetch on network reconnect */
  refetchOnReconnect?: boolean;
  /** Transform the hook result */
  selectFromResult?: (result: UseQueryStateDefaultResult<ResultType>) => any;
}

interface UseQueryResult<ResultType> {
  /** Query result data */
  data: ResultType | undefined;
  /** Current query error */
  error: any;
  /** True during initial load */
  isLoading: boolean;
  /** True during any fetch operation */
  isFetching: boolean;
  /** True when query succeeded */
  isSuccess: boolean;
  /** True when query failed */
  isError: boolean;
  /** True when query has never been run */
  isUninitialized: boolean;
  /** Current query status */
  status: QueryStatus;
  /** Current request ID */
  requestId: string;
  /** Request start timestamp */
  startedTimeStamp?: number;
  /** Request fulfillment timestamp */
  fulfilledTimeStamp?: number;
  /** Current query arguments */
  originalArgs?: any;
  /** Manual refetch function */
  refetch: () => QueryActionCreatorResult<any>;
}

/**
 * Auto-generated lazy query hook for manual triggering
 * Generated for each query endpoint as `useLazy{EndpointName}Query`
 */
type UseLazyQueryHook<ResultType, QueryArg> = (
  options?: UseLazyQueryOptions
) => [
  (arg: QueryArg, preferCacheValue?: boolean) => QueryActionCreatorResult<ResultType>,
  UseQueryResult<ResultType>
];

interface UseLazyQueryOptions {
  /** Transform the hook result */
  selectFromResult?: (result: UseQueryStateDefaultResult<any>) => any;
}

/**
 * Auto-generated mutation hook for data modification
 * Generated for each mutation endpoint as `use{EndpointName}Mutation`
 */
type UseMutationHook<ResultType, QueryArg> = (
  options?: UseMutationOptions
) => [
  (arg: QueryArg) => MutationActionCreatorResult<ResultType>,
  UseMutationResult<ResultType>
];

interface UseMutationOptions {
  /** Transform the hook result */
  selectFromResult?: (result: UseMutationStateDefaultResult<any>) => any;
}

interface UseMutationResult<ResultType> {
  /** Mutation result data */
  data: ResultType | undefined;
  /** Mutation error */
  error: any;
  /** True during mutation */
  isLoading: boolean;
  /** True when mutation succeeded */
  isSuccess: boolean;
  /** True when mutation failed */
  isError: boolean;
  /** True when mutation has never been called */
  isUninitialized: boolean;
  /** Current mutation status */
  status: QueryStatus;
  /** Reset mutation state */
  reset: () => void;
  /** Original arguments passed to mutation */
  originalArgs?: any;
  /** Request start timestamp */
  startedTimeStamp?: number;
  /** Request fulfillment timestamp */
  fulfilledTimeStamp?: number;
}

Usage Examples:

import { api } from './api';

// Using auto-generated query hooks
const PostsList = () => {
  const { 
    data: posts, 
    error, 
    isLoading, 
    isFetching,
    refetch 
  } = useGetPostsQuery();
  
  const { 
    data: user,
    isLoading: userLoading 
  } = useGetCurrentUserQuery(undefined, {
    skip: !posts, // Skip until posts are loaded
    pollingInterval: 60000 // Poll every minute
  });

  if (isLoading) return <div>Loading posts...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <button onClick={refetch} disabled={isFetching}>
        {isFetching ? 'Refreshing...' : 'Refresh'}
      </button>
      {posts?.map(post => (
        <PostItem key={post.id} post={post} />
      ))}
    </div>
  );
};

// Using lazy query hook
const SearchComponent = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const [triggerSearch, { data: results, isLoading, error }] = useLazyGetSearchResultsQuery();
  
  const handleSearch = () => {
    if (searchTerm.trim()) {
      triggerSearch(searchTerm);
    }
  };
  
  return (
    <div>
      <input 
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
      />
      <button onClick={handleSearch} disabled={isLoading}>
        {isLoading ? 'Searching...' : 'Search'}
      </button>
      {results && (
        <div>
          {results.map(item => <SearchResult key={item.id} item={item} />)}
        </div>
      )}
    </div>
  );
};

// Using mutation hook
const AddPostForm = () => {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [addPost, { isLoading, error, isSuccess }] = useAddPostMutation();
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await addPost({ title, content }).unwrap();
      setTitle('');
      setContent('');
    } catch (error) {
      console.error('Failed to add post:', error);
    }
  };
  
  useEffect(() => {
    if (isSuccess) {
      alert('Post added successfully!');
    }
  }, [isSuccess]);
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Post title"
        required
      />
      <textarea 
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="Post content"
        required
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Adding...' : 'Add Post'}
      </button>
      {error && <div>Error: {error.message}</div>}
    </form>
  );
};

Query State Management Hooks

Additional hooks for fine-grained control over query state without triggering new requests.

/**
 * Hook to access query state without subscription
 * Generated for each query endpoint as `use{EndpointName}QueryState`
 */
type UseQueryStateHook<ResultType, QueryArg> = (
  arg: QueryArg,
  options?: UseQueryStateOptions
) => UseQueryResult<ResultType>;

interface UseQueryStateOptions {
  /** Skip the hook */
  skip?: boolean;
  /** Transform the hook result */
  selectFromResult?: (result: UseQueryStateDefaultResult<any>) => any;
}

/**
 * Hook for query subscription without data access
 * Generated for each query endpoint as `use{EndpointName}QuerySubscription`
 */
type UseQuerySubscriptionHook<QueryArg> = (
  arg: QueryArg,
  options?: UseQuerySubscriptionOptions<QueryArg>
) => Pick<UseQueryResult<any>, 'refetch'>;

interface UseQuerySubscriptionOptions<QueryArg> extends UseQueryOptions<QueryArg> {
  /** Skip the subscription */
  skip?: boolean;
}

/**
 * Lazy version of query subscription hook
 * Generated for each query endpoint as `useLazy{EndpointName}QuerySubscription`
 */
type UseLazyQuerySubscriptionHook<QueryArg> = () => [
  (arg: QueryArg) => QueryActionCreatorResult<any>,
  Pick<UseQueryResult<any>, 'refetch'>
];

Usage Examples:

// Separate data access and subscription
const PostsListOptimized = () => {
  // Subscribe to updates but don't access data here
  const { refetch } = useGetPostsQuerySubscription();
  
  return (
    <div>
      <PostsListData />
      <button onClick={refetch}>Refresh</button>
    </div>
  );
};

const PostsListData = () => {
  // Access data without creating new subscription
  const { data: posts, isLoading } = useGetPostsQueryState();
  
  if (isLoading) return <div>Loading...</div>;
  
  return (
    <div>
      {posts?.map(post => <PostItem key={post.id} post={post} />)}
    </div>
  );
};

// Conditional subscriptions
const ConditionalDataComponent = ({ userId }: { userId?: string }) => {
  // Only subscribe when we have a user ID
  useGetUserDataQuerySubscription(userId!, {
    skip: !userId,
    pollingInterval: 30000
  });
  
  // Access the data separately
  const { data: userData } = useGetUserDataQueryState(userId!, {
    skip: !userId
  });
  
  return userId ? <UserProfile user={userData} /> : <div>No user selected</div>;
};

Result Selection and Transformation

Transform and select specific parts of query results for optimized re-renders.

/**
 * Transform query result before returning from hook
 */
interface SelectFromResultOptions<T> {
  selectFromResult?: (result: UseQueryStateDefaultResult<T>) => any;
}

interface UseQueryStateDefaultResult<T> {
  data: T | undefined;
  error: any;
  isLoading: boolean;
  isFetching: boolean;
  isSuccess: boolean;
  isError: boolean;
  isUninitialized: boolean;
  status: QueryStatus;
  requestId: string;
  startedTimeStamp?: number;
  fulfilledTimeStamp?: number;
  originalArgs?: any;
  refetch: () => QueryActionCreatorResult<any>;
}

Usage Examples:

// Select only specific data to minimize re-renders
const UserName = ({ userId }: { userId: string }) => {
  const userName = useGetUserQuery(userId, {
    selectFromResult: ({ data, isLoading, error }) => ({
      name: data?.name,
      isLoading,
      error
    })
  });
  
  // Component only re-renders when name, loading state, or error changes
  if (userName.isLoading) return <div>Loading...</div>;
  if (userName.error) return <div>Error loading user</div>;
  
  return <div>{userName.name}</div>;
};

// Select derived data
const PostsStats = () => {
  const stats = useGetPostsQuery(undefined, {
    selectFromResult: ({ data, isLoading }) => ({
      totalPosts: data?.length ?? 0,
      publishedPosts: data?.filter(p => p.published).length ?? 0,
      isLoading
    })
  });
  
  return (
    <div>
      <p>Total: {stats.totalPosts}</p>
      <p>Published: {stats.publishedPosts}</p>
    </div>
  );
};

// Combine multiple query results
const DashboardSummary = () => {
  const summary = useCombinedQueries();
  
  return <div>{JSON.stringify(summary)}</div>;
};

const useCombinedQueries = () => {
  const postsResult = useGetPostsQuery();
  const usersResult = useGetUsersQuery();
  const commentsResult = useGetCommentsQuery();
  
  return useMemo(() => {
    if (postsResult.isLoading || usersResult.isLoading || commentsResult.isLoading) {
      return { isLoading: true };
    }
    
    if (postsResult.error || usersResult.error || commentsResult.error) {
      return { 
        error: postsResult.error || usersResult.error || commentsResult.error 
      };
    }
    
    return {
      isLoading: false,
      data: {
        totalPosts: postsResult.data?.length ?? 0,
        totalUsers: usersResult.data?.length ?? 0,
        totalComments: commentsResult.data?.length ?? 0,
        latestPost: postsResult.data?.[0],
        activeUsers: usersResult.data?.filter(u => u.isActive).length ?? 0
      }
    };
  }, [postsResult, usersResult, commentsResult]);
};

API Provider Component

Standalone provider for using RTK Query without Redux store setup.

/**
 * Provider component for standalone RTK Query usage
 * @param props - Provider configuration
 * @returns JSX element wrapping children with RTK Query context
 */
function ApiProvider<A extends Api<any, {}, any, any>>(props: {
  /** RTK Query API instance */
  api: A;
  /** Enable automatic listeners setup */
  setupListeners?: boolean | ((dispatch: ThunkDispatch<any, any, any>) => () => void);
  /** React children */
  children: React.ReactNode;
}): JSX.Element;

Usage Examples:

import { ApiProvider } from '@reduxjs/toolkit/query/react';
import { api } from './api';

// Standalone RTK Query usage without Redux store
const App = () => {
  return (
    <ApiProvider 
      api={api}
      setupListeners={true} // Enable automatic refetch on focus/reconnect
    >
      <PostsList />
      <AddPostForm />
    </ApiProvider>
  );
};

// Custom listener setup
const AppWithCustomListeners = () => {
  const setupCustomListeners = useCallback((dispatch) => {
    // Custom listener setup
    const unsubscribe = setupListeners(dispatch, {
      onFocus: () => console.log('App focused'),
      onOnline: () => console.log('App online')
    });
    
    return unsubscribe;
  }, []);
  
  return (
    <ApiProvider 
      api={api}
      setupListeners={setupCustomListeners}
    >
      <AppContent />
    </ApiProvider>
  );
};

// Multiple API providers
const MultiApiApp = () => {
  return (
    <ApiProvider api={postsApi}>
      <ApiProvider api={usersApi}>
        <AppContent />
      </ApiProvider>
    </ApiProvider>
  );
};

Infinite Query Hooks

Special hooks for handling paginated/infinite data patterns.

/**
 * Auto-generated infinite query hook
 * Generated for each infinite query endpoint as `use{EndpointName}InfiniteQuery`
 */
type UseInfiniteQueryHook<ResultType, QueryArg, PageParam> = (
  arg: QueryArg,
  options?: UseInfiniteQueryOptions<QueryArg>
) => UseInfiniteQueryResult<ResultType, PageParam>;

interface UseInfiniteQueryOptions<QueryArg> extends UseQueryOptions<QueryArg> {
  /** Maximum number of pages to keep in cache */
  maxPages?: number;
}

interface UseInfiniteQueryResult<ResultType, PageParam> {
  /** All pages of data */
  data: ResultType | undefined;
  /** Current query error */
  error: any;
  /** Loading states */
  isLoading: boolean;
  isFetching: boolean;
  isFetchingNextPage: boolean;
  isFetchingPreviousPage: boolean;
  /** Success/error states */
  isSuccess: boolean;
  isError: boolean;
  /** Pagination info */
  hasNextPage: boolean;
  hasPreviousPage: boolean;
  /** Pagination actions */
  fetchNextPage: () => void;
  fetchPreviousPage: () => void;
  /** Refetch all pages */
  refetch: () => void;
}

Usage Examples:

// Define infinite query endpoint
const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getPostsInfinite: builder.infiniteQuery<
      { posts: Post[]; total: number; page: number },
      { limit: number },
      number
    >({
      query: ({ limit = 10 }) => ({
        url: 'posts',
        params: { limit, offset: 0 }
      }),
      getNextPageParam: (lastPage, allPages) => {
        const totalLoaded = allPages.length * 10;
        return totalLoaded < lastPage.total ? allPages.length + 1 : undefined;
      },
      getCombinedResult: (pages) => ({
        posts: pages.flatMap(page => page.posts),
        total: pages[0]?.total ?? 0,
        currentPage: pages.length
      })
    })
  })
});

// Using infinite query hook
const InfinitePostsList = () => {
  const {
    data,
    error,
    isLoading,
    isFetchingNextPage,
    hasNextPage,
    fetchNextPage
  } = useGetPostsInfiniteQuery({ limit: 10 });
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      {data?.posts.map(post => (
        <PostItem key={post.id} post={post} />
      ))}
      
      {hasNextPage && (
        <button 
          onClick={fetchNextPage}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading more...' : 'Load More'}
        </button>
      )}
    </div>
  );
};

// Infinite scroll implementation
const InfiniteScrollPosts = () => {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage
  } = useGetPostsInfiniteQuery({ limit: 20 });
  
  const loadMoreRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
          fetchNextPage();
        }
      },
      { threshold: 1.0 }
    );
    
    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current);
    }
    
    return () => observer.disconnect();
  }, [fetchNextPage, hasNextPage, isFetchingNextPage]);
  
  return (
    <div>
      {data?.posts.map(post => (
        <PostItem key={post.id} post={post} />
      ))}
      <div ref={loadMoreRef}>
        {isFetchingNextPage && <div>Loading more posts...</div>}
      </div>
    </div>
  );
};

Advanced Patterns

Optimistic Updates with Error Recovery

const OptimisticPostEditor = ({ postId }: { postId: string }) => {
  const [updatePost] = useUpdatePostMutation();
  const queryClient = useQueryClient();
  
  const handleOptimisticUpdate = async (changes: Partial<Post>) => {
    // Store original data for potential rollback
    const previousPost = queryClient.getQueryData(['post', postId]);
    
    // Optimistically update UI
    queryClient.setQueryData(['post', postId], (old: Post | undefined) => 
      old ? { ...old, ...changes } : undefined
    );
    
    try {
      await updatePost({ id: postId, patch: changes }).unwrap();
    } catch (error) {
      // Rollback on error
      queryClient.setQueryData(['post', postId], previousPost);
      throw error;
    }
  };
  
  return <PostEditor onSave={handleOptimisticUpdate} />;
};

Synchronized Queries

// Keep multiple related queries in sync
const SynchronizedDataComponent = ({ userId }: { userId: string }) => {
  const { data: user } = useGetUserQuery(userId);
  const { data: posts } = useGetUserPostsQuery(userId, {
    skip: !user // Wait for user data
  });
  const { data: profile } = useGetUserProfileQuery(userId, {
    skip: !user
  });
  
  // All queries are synchronized - profile and posts only load after user
  return (
    <div>
      {user && <UserInfo user={user} />}
      {posts && <PostsList posts={posts} />}
      {profile && <UserProfile profile={profile} />}
    </div>
  );
};

Custom Hook Composition

// Compose multiple RTK Query hooks into custom hooks
const usePostWithAuthor = (postId: string) => {
  const { data: post, ...postQuery } = useGetPostQuery(postId);
  const { data: author, ...authorQuery } = useGetUserQuery(post?.authorId!, {
    skip: !post?.authorId
  });
  
  return {
    post,
    author,
    isLoading: postQuery.isLoading || authorQuery.isLoading,
    error: postQuery.error || authorQuery.error,
    refetch: () => {
      postQuery.refetch();
      if (post?.authorId) {
        authorQuery.refetch();
      }
    }
  };
};

// Usage
const PostWithAuthorComponent = ({ postId }: { postId: string }) => {
  const { post, author, isLoading, error } = usePostWithAuthor(postId);
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading post</div>;
  
  return (
    <article>
      <h1>{post?.title}</h1>
      <p>By {author?.name}</p>
      <div>{post?.content}</div>
    </article>
  );
};

RTK Query React Constants

Constants and utilities specific to RTK Query React integration.

/**
 * Special value representing uninitialized query state
 * Used internally by RTK Query React hooks
 */
const UNINITIALIZED_VALUE: unique symbol;

Usage Examples:

import { UNINITIALIZED_VALUE } from '@reduxjs/toolkit/query/react';

// Check if query result is truly uninitialized (vs undefined data)
const MyComponent = () => {
  const { data, isUninitialized } = useGetDataQuery();
  
  // Direct comparison (rarely needed in application code)
  if (data === UNINITIALIZED_VALUE) {
    console.log('Query has not been started');
  }
  
  // Prefer using the isUninitialized flag
  if (isUninitialized) {
    return <div>Query not started</div>;
  }
  
  return <div>{data ? 'Has data' : 'No data'}</div>;
};

Install with Tessl CLI

npx tessl i tessl/npm-reduxjs--toolkit

docs

actions-reducers.md

async-thunks.md

core-store.md

entity-adapters.md

index.md

middleware.md

react-integration.md

rtk-query-react.md

rtk-query.md

utilities.md

tile.json