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.mddocs/

RTK Query Core

RTK Query provides a powerful data fetching solution with automatic caching, background updates, and comprehensive state management. Available from @reduxjs/toolkit/query.

Capabilities

Create API

Creates an RTK Query API slice with endpoints for data fetching and caching.

/**
 * Creates RTK Query API slice with endpoints for data fetching
 * @param options - API configuration options
 * @returns API object with endpoints, reducer, and middleware
 */
function createApi<
  BaseQuery extends BaseQueryFn,
  Definitions extends EndpointDefinitions,
  ReducerPath extends string = 'api',
  TagTypes extends string = never
>(options: CreateApiOptions<BaseQuery, Definitions, ReducerPath, TagTypes>): Api<BaseQuery, Definitions, ReducerPath, TagTypes>;

interface CreateApiOptions<BaseQuery, Definitions, ReducerPath, TagTypes> {
  /** Unique key for the API slice in Redux state */
  reducerPath?: ReducerPath;
  
  /** Base query function for making requests */
  baseQuery: BaseQuery;
  
  /** Cache tag type names for invalidation */
  tagTypes?: readonly TagTypes[];
  
  /** Endpoint definitions */
  endpoints: (builder: EndpointBuilder<BaseQuery, TagTypes, ReducerPath>) => Definitions;
  
  /** Custom query argument serialization */
  serializeQueryArgs?: SerializeQueryArgs<any>;
  
  /** Global cache retention time in seconds */
  keepUnusedDataFor?: number;
  
  /** Global refetch behavior on mount or arg change */
  refetchOnMountOrArgChange?: boolean | number;
  
  /** Global refetch on window focus */
  refetchOnFocus?: boolean;
  
  /** Global refetch on network reconnect */
  refetchOnReconnect?: boolean;
}

interface Api<BaseQuery, Definitions, ReducerPath, TagTypes> {
  /** Generated endpoint objects with query/mutation methods */
  endpoints: ApiEndpoints<Definitions, BaseQuery, TagTypes>;
  
  /** Reducer function for the API slice */
  reducer: Reducer<CombinedState<Definitions, TagTypes, ReducerPath>>;
  
  /** Middleware for handling async operations */
  middleware: Middleware<{}, any, Dispatch<AnyAction>>;
  
  /** Utility functions */
  util: {
    /** Get running query promises */
    getRunningOperationPromises(): Record<string, QueryActionCreatorResult<any> | MutationActionCreatorResult<any>>;
    /** Reset API state */
    resetApiState(): PayloadAction<void>;
    /** Invalidate cache tags */
    invalidateTags(tags: readonly TagDescription<TagTypes>[]): PayloadAction<TagDescription<TagTypes>[]>;
    /** Select cache entries by tags */
    selectCachedArgsForQuery<T>(state: any, endpointName: T): readonly unknown[];
    /** Select invalidated cache entries */
    selectInvalidatedBy(state: any, tags: readonly TagDescription<TagTypes>[]): readonly unknown[];
  };
  
  /** Internal cache key functions */
  internalActions: {
    onOnline(): PayloadAction<void>;
    onOffline(): PayloadAction<void>;
    onFocus(): PayloadAction<void>;
    onFocusLost(): PayloadAction<void>;
  };
}

Usage Examples:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';

// Basic API definition
const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api/',
  }),
  tagTypes: ['Post', 'User'],
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], void>({
      query: () => 'posts',
      providesTags: ['Post']
    }),
    getPost: builder.query<Post, number>({
      query: (id) => `posts/${id}`,
      providesTags: (result, error, id) => [{ type: 'Post', id }]
    }),
    addPost: builder.mutation<Post, Partial<Post>>({
      query: (newPost) => ({
        url: 'posts',
        method: 'POST',
        body: newPost,
      }),
      invalidatesTags: ['Post']
    }),
    updatePost: builder.mutation<Post, { id: number; patch: Partial<Post> }>({
      query: ({ id, patch }) => ({
        url: `posts/${id}`,
        method: 'PATCH',
        body: patch,
      }),
      invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]
    }),
    deletePost: builder.mutation<{ success: boolean; id: number }, number>({
      query: (id) => ({
        url: `posts/${id}`,
        method: 'DELETE',
      }),
      invalidatesTags: (result, error, id) => [{ type: 'Post', id }]
    })
  })
});

// Export hooks and actions
export const {
  useGetPostsQuery,
  useGetPostQuery,
  useAddPostMutation,
  useUpdatePostMutation,
  useDeletePostMutation
} = postsApi;

// Configure store
const store = configureStore({
  reducer: {
    postsApi: postsApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(postsApi.middleware)
});

Base Query Functions

RTK Query provides base query implementations for common scenarios.

/**
 * Built-in base query using fetch API
 * @param options - Fetch configuration options
 * @returns Base query function for HTTP requests
 */
function fetchBaseQuery(options?: FetchBaseQueryArgs): BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError>;

interface FetchBaseQueryArgs {
  /** Base URL for all requests */
  baseUrl?: string;
  
  /** Function to prepare request headers */
  prepareHeaders?: (headers: Headers, api: { getState: () => unknown; extra: any; endpoint: string; type: 'query' | 'mutation'; forced?: boolean }) => Headers | void;
  
  /** Custom fetch implementation */
  fetchFn?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
  
  /** URL parameter serialization function */
  paramsSerializer?: (params: Record<string, any>) => string;
  
  /** Request timeout in milliseconds */
  timeout?: number;
  
  /** Response validation function */
  validateStatus?: (response: Response, body: any) => boolean;
}

interface FetchArgs {
  /** Request URL */
  url: string;
  /** HTTP method */
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
  /** Request headers */
  headers?: HeadersInit;
  /** Request body */
  body?: any;
  /** URL parameters */
  params?: Record<string, any>;
  /** Response parsing method */
  responseHandler?: 'content-type' | 'json' | 'text' | ((response: Response) => Promise<any>);
  /** Response validation */
  validateStatus?: (response: Response, body: any) => boolean;
  /** Request timeout */
  timeout?: number;
}

type FetchBaseQueryError =
  | { status: number; data: any }
  | { status: 'FETCH_ERROR'; error: string; data?: undefined }
  | { status: 'PARSING_ERROR'; originalStatus: number; data: string; error: string }
  | { status: 'TIMEOUT_ERROR'; error: string; data?: undefined }
  | { status: 'CUSTOM_ERROR'; error: string; data?: any };

/**
 * Placeholder base query for APIs that only use queryFn
 * @returns Fake base query that throws error if used
 */
function fakeBaseQuery<ErrorType = string>(): BaseQueryFn<any, any, ErrorType>;

Usage Examples:

// Basic fetch base query
const api = createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: 'https://api.example.com/',
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth.token;
      if (token) {
        headers.set('authorization', `Bearer ${token}`);
      }
      headers.set('content-type', 'application/json');
      return headers;
    },
    timeout: 10000
  }),
  endpoints: (builder) => ({
    getData: builder.query<Data, string>({
      query: (id) => ({
        url: `data/${id}`,
        params: { include: 'details' }
      })
    })
  })
});

// Custom response handling
const apiWithCustomResponse = createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: '/api/',
    validateStatus: (response, result) => {
      return response.ok && result?.success === true;
    }
  }),
  endpoints: (builder) => ({
    getProtectedData: builder.query<ProtectedData, void>({
      query: () => ({
        url: 'protected',
        responseHandler: async (response) => {
          if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
          }
          const data = await response.json();
          return data.payload; // Extract nested payload
        }
      })
    })
  })
});

// Using fakeBaseQuery with queryFn
const apiWithCustomLogic = createApi({
  baseQuery: fakeBaseQuery<CustomError>(),
  endpoints: (builder) => ({
    getComplexData: builder.query<ComplexData, string>({
      queryFn: async (arg, queryApi, extraOptions, baseQuery) => {
        try {
          // Custom logic for data fetching
          const step1 = await customApiClient.fetchUserData(arg);
          const step2 = await customApiClient.fetchUserPreferences(step1.id);
          
          return { data: { ...step1, preferences: step2 } };
        } catch (error) {
          return { error: { status: 'CUSTOM_ERROR', error: error.message } };
        }
      }
    })
  })
});

Endpoint Definitions

Define query and mutation endpoints with comprehensive configuration options.

/**
 * Builder object for defining endpoints
 */
interface EndpointBuilder<BaseQuery, TagTypes, ReducerPath> {
  /** Define a query endpoint for data fetching */
  query<ResultType, QueryArg = void>(
    definition: QueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath>
  ): QueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath>;
  
  /** Define a mutation endpoint for data modification */
  mutation<ResultType, QueryArg = void>(
    definition: MutationDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath>
  ): MutationDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath>;
  
  /** Define an infinite query endpoint for paginated data */
  infiniteQuery<ResultType, QueryArg = void, PageParam = unknown>(
    definition: InfiniteQueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, PageParam, ReducerPath>
  ): InfiniteQueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, PageParam, ReducerPath>;
}

interface QueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath> {
  /** Query function or args */
  query: (arg: QueryArg) => any;
  
  /** Custom query implementation */
  queryFn?: (arg: QueryArg, api: QueryApi<ReducerPath, any, any, any>, extraOptions: any, baseQuery: BaseQuery) => Promise<QueryFnResult<ResultType>> | QueryFnResult<ResultType>;
  
  /** Transform the response data */
  transformResponse?: (baseQueryReturnValue: any, meta: any, arg: QueryArg) => ResultType | Promise<ResultType>;
  
  /** Transform error responses */
  transformErrorResponse?: (baseQueryReturnValue: any, meta: any, arg: QueryArg) => any;
  
  /** Cache tags this query provides */
  providesTags?: ResultDescription<TagTypes, ResultType, QueryArg>;
  
  /** Cache retention time override */
  keepUnusedDataFor?: number;
  
  /** Lifecycle callback when query starts */
  onQueryStarted?: (arg: QueryArg, api: QueryLifecycleApi<QueryArg, BaseQuery, ResultType, ReducerPath>) => Promise<void> | void;
  
  /** Lifecycle callback when cache entry is added */
  onCacheEntryAdded?: (arg: QueryArg, api: CacheLifecycleApi<QueryArg, BaseQuery, ResultType, ReducerPath>) => Promise<void> | void;
}

interface MutationDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath> {
  /** Mutation function or args */
  query: (arg: QueryArg) => any;
  
  /** Custom mutation implementation */
  queryFn?: (arg: QueryArg, api: QueryApi<ReducerPath, any, any, any>, extraOptions: any, baseQuery: BaseQuery) => Promise<QueryFnResult<ResultType>> | QueryFnResult<ResultType>;
  
  /** Transform the response data */
  transformResponse?: (baseQueryReturnValue: any, meta: any, arg: QueryArg) => ResultType | Promise<ResultType>;
  
  /** Transform error responses */
  transformErrorResponse?: (baseQueryReturnValue: any, meta: any, arg: QueryArg) => any;
  
  /** Cache tags to invalidate */
  invalidatesTags?: ResultDescription<TagTypes, ResultType, QueryArg>;
  
  /** Lifecycle callback when mutation starts */
  onQueryStarted?: (arg: QueryArg, api: MutationLifecycleApi<QueryArg, BaseQuery, ResultType, ReducerPath>) => Promise<void> | void;
  
  /** Lifecycle callback when cache entry is added */
  onCacheEntryAdded?: (arg: QueryArg, api: CacheLifecycleApi<QueryArg, BaseQuery, ResultType, ReducerPath>) => Promise<void> | void;
}

type ResultDescription<TagTypes, ResultType, QueryArg> =
  | readonly TagTypes[]
  | readonly TagDescription<TagTypes>[]
  | ((result: ResultType | undefined, error: any, arg: QueryArg) => readonly TagDescription<TagTypes>[]);

interface TagDescription<TagTypes> {
  type: TagTypes;
  id?: string | number;
}

interface QueryFnResult<T> {
  data?: T;
  error?: any;
  meta?: any;
}

Usage Examples:

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Post', 'User', 'Comment'],
  endpoints: (builder) => ({
    // Simple query
    getPosts: builder.query<Post[], void>({
      query: () => 'posts',
      providesTags: ['Post']
    }),
    
    // Query with parameters
    getPostsByUser: builder.query<Post[], { userId: string; limit?: number }>({
      query: ({ userId, limit = 10 }) => ({
        url: 'posts',
        params: { userId, limit }
      }),
      providesTags: (result, error, { userId }) => [
        'Post',
        { type: 'User', id: userId }
      ]
    }),
    
    // Query with response transformation
    getPostWithComments: builder.query<PostWithComments, string>({
      query: (id) => `posts/${id}`,
      transformResponse: (response: { post: Post; comments: Comment[] }) => ({
        ...response.post,
        comments: response.comments.sort((a, b) => 
          new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
        )
      }),
      providesTags: (result, error, id) => [
        { type: 'Post', id },
        { type: 'Comment', id: 'LIST' }
      ]
    }),
    
    // Custom query function
    getAnalytics: builder.query<Analytics, { startDate: string; endDate: string }>({
      queryFn: async (arg, queryApi, extraOptions, baseQuery) => {
        try {
          // Multiple API calls
          const [posts, users, comments] = await Promise.all([
            baseQuery(`analytics/posts?start=${arg.startDate}&end=${arg.endDate}`),
            baseQuery(`analytics/users?start=${arg.startDate}&end=${arg.endDate}`),
            baseQuery(`analytics/comments?start=${arg.startDate}&end=${arg.endDate}`)
          ]);
          
          if (posts.error || users.error || comments.error) {
            return { error: 'Failed to fetch analytics data' };
          }
          
          return {
            data: {
              posts: posts.data,
              users: users.data,
              comments: comments.data,
              summary: calculateSummary(posts.data, users.data, comments.data)
            }
          };
        } catch (error) {
          return { error: error.message };
        }
      }
    }),
    
    // Mutation with optimistic updates
    updatePost: builder.mutation<Post, { id: string; patch: Partial<Post> }>({
      query: ({ id, patch }) => ({
        url: `posts/${id}`,
        method: 'PATCH',
        body: patch
      }),
      onQueryStarted: async ({ id, patch }, { dispatch, queryFulfilled }) => {
        // Optimistic update
        const patchResult = dispatch(
          api.util.updateQueryData('getPost', id, (draft) => {
            Object.assign(draft, patch);
          })
        );
        
        try {
          await queryFulfilled;
        } catch {
          // Revert on failure
          patchResult.undo();
        }
      },
      invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]
    }),
    
    // Mutation with side effects
    deletePost: builder.mutation<void, string>({
      query: (id) => ({
        url: `posts/${id}`,
        method: 'DELETE'
      }),
      onQueryStarted: async (id, { dispatch, queryFulfilled }) => {
        try {
          await queryFulfilled;
          
          // Additional side effects
          dispatch(showNotification('Post deleted successfully'));
          dispatch(api.util.invalidateTags([{ type: 'Post', id }]));
        } catch (error) {
          dispatch(showNotification('Failed to delete post'));
        }
      }
    })
  })
});

Cache Management

RTK Query provides sophisticated caching with tag-based invalidation and cache lifecycle management.

/**
 * Skip query execution when passed as argument
 */
const skipToken: unique symbol;

/**
 * Query status enumeration
 */
enum QueryStatus {
  uninitialized = 'uninitialized',
  pending = 'pending',
  fulfilled = 'fulfilled',
  rejected = 'rejected'
}

/**
 * Cache state structure for queries and mutations
 */
interface QueryState<T> {
  /** Query status */
  status: QueryStatus;
  /** Query result data */
  data?: T;
  /** Current request ID */
  requestId?: string;
  /** Error information */
  error?: any;
  /** Fulfillment timestamp */
  fulfilledTimeStamp?: number;
  /** Start timestamp */
  startedTimeStamp?: number;
  /** End timestamp */
  endedTimeStamp?: number;
}

interface MutationState<T> {
  /** Mutation status */
  status: QueryStatus;
  /** Mutation result data */
  data?: T;
  /** Current request ID */
  requestId?: string;
  /** Error information */
  error?: any;
}

Usage Examples:

// Using skipToken to conditionally skip queries
const UserProfile = ({ userId }: { userId?: string }) => {
  const { data: user, error, isLoading } = useGetUserQuery(
    userId ?? skipToken // Skip query if no userId
  );
  
  if (!userId) {
    return <div>Please select a user</div>;
  }
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading user</div>;
  
  return <div>{user?.name}</div>;
};

// Manual cache updates
const PostsList = () => {
  const { data: posts } = useGetPostsQuery();
  const [updatePost] = useUpdatePostMutation();
  
  const handleOptimisticUpdate = (id: string, changes: Partial<Post>) => {
    // Manually update cache
    store.dispatch(
      api.util.updateQueryData('getPosts', undefined, (draft) => {
        const post = draft.find(p => p.id === id);
        if (post) {
          Object.assign(post, changes);
        }
      })
    );
    
    // Then perform actual update
    updatePost({ id, patch: changes });
  };
  
  return (
    <div>
      {posts?.map(post => (
        <PostItem 
          key={post.id} 
          post={post}
          onUpdate={handleOptimisticUpdate}
        />
      ))}
    </div>
  );
};

// Prefetching data
const PostsListWithPrefetch = () => {
  const dispatch = useAppDispatch();
  
  useEffect(() => {
    // Prefetch data
    dispatch(api.util.prefetch('getPosts', undefined, { force: false }));
    
    // Prefetch related data
    dispatch(api.util.prefetch('getUsers', undefined));
  }, [dispatch]);
  
  const { data: posts } = useGetPostsQuery();
  
  return <div>...</div>;
};

Advanced Features

Streaming Updates

// Real-time updates with cache entry lifecycle
const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], void>({
      query: () => 'posts',
      onCacheEntryAdded: async (
        arg,
        { updateCachedData, cacheDataLoaded, cacheEntryRemoved }
      ) => {
        // Wait for initial data load
        await cacheDataLoaded;
        
        // Setup WebSocket connection
        const ws = new WebSocket('ws://localhost:8080/posts');
        
        ws.addEventListener('message', (event) => {
          const update = JSON.parse(event.data);
          
          updateCachedData((draft) => {
            switch (update.type) {
              case 'POST_ADDED':
                draft.push(update.post);
                break;
              case 'POST_UPDATED':
                const index = draft.findIndex(p => p.id === update.post.id);
                if (index !== -1) {
                  draft[index] = update.post;
                }
                break;
              case 'POST_DELETED':
                return draft.filter(p => p.id !== update.postId);
            }
          });
        });
        
        // Cleanup on cache entry removal
        await cacheEntryRemoved;
        ws.close();
      }
    })
  })
});

Request Deduplication and Polling

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    // Polling query
    getLiveData: builder.query<LiveData, void>({
      query: () => 'live-data',
      // Automatic polling every 5 seconds
      pollingInterval: 5000
    }),
    
    // Custom request deduplication
    getExpensiveData: builder.query<ExpensiveData, string>({
      query: (id) => `expensive-data/${id}`,
      serializeQueryArgs: ({ queryArgs, endpointDefinition, endpointName }) => {
        // Custom serialization for deduplication
        return `${endpointName}(${queryArgs})`;
      },
      // Cache for 10 minutes
      keepUnusedDataFor: 600
    })
  })
});

// Component with polling control
const LiveDataComponent = () => {
  const [pollingEnabled, setPollingEnabled] = useState(true);
  
  const { data, error, isLoading } = useGetLiveDataQuery(undefined, {
    pollingInterval: pollingEnabled ? 5000 : 0,
    skipPollingIfUnfocused: true
  });
  
  return (
    <div>
      <button onClick={() => setPollingEnabled(!pollingEnabled)}>
        {pollingEnabled ? 'Stop' : 'Start'} Polling
      </button>
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
};

Background Refetching

// Setup automatic refetching listeners
import { setupListeners } from '@reduxjs/toolkit/query';

// Enable refetching on focus/reconnect
setupListeners(store.dispatch, {
  onFocus: () => console.log('Window focused'),
  onFocusLost: () => console.log('Window focus lost'),
  onOnline: () => console.log('Network online'),
  onOffline: () => console.log('Network offline')
});

// API with custom refetch behavior
const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  refetchOnFocus: true,
  refetchOnReconnect: true,
  endpoints: (builder) => ({
    getCriticalData: builder.query<CriticalData, void>({
      query: () => 'critical-data',
      // Always refetch on mount
      refetchOnMountOrArgChange: true
    }),
    
    getCachedData: builder.query<CachedData, void>({
      query: () => 'cached-data',
      // Only refetch if older than 5 minutes
      refetchOnMountOrArgChange: 300
    })
  })
});

RTK Query Utilities

Additional utility functions for advanced RTK Query usage.

/**
 * Default function for serializing query arguments into cache keys
 * @param args - Query arguments to serialize
 * @param endpointDefinition - Endpoint definition for context
 * @param endpointName - Name of the endpoint
 * @returns Serialized string representation of arguments
 */
function defaultSerializeQueryArgs(
  args: any,
  endpointDefinition: EndpointDefinition<any, any, any, any>,
  endpointName: string
): string;

/**
 * Copy value while preserving structural sharing for performance
 * Used internally for immutable updates with minimal re-renders
 * @param oldObj - Previous value
 * @param newObj - New value to merge
 * @returns Value with structural sharing where possible
 */
function copyWithStructuralSharing<T>(oldObj: T, newObj: T): T;

/**
 * Retry utility for enhanced error handling in base queries
 * @param baseQuery - Base query function to retry
 * @param options - Retry configuration options
 * @returns Enhanced base query with retry logic
 */
function retry<Args extends any, Return extends any, Error extends any>(
  baseQuery: BaseQueryFn<Args, Return, Error>,
  options: RetryOptions
): BaseQueryFn<Args, Return, Error>;

interface RetryOptions {
  /** Maximum number of retry attempts */
  maxRetries: number;
  /** Function to determine if error should trigger retry */
  retryCondition?: (error: any, args: any, extraOptions: any) => boolean;
  /** Delay between retries in milliseconds */
  backoff?: (attempt: number, maxRetries: number) => number;
}

Usage Examples:

import { 
  defaultSerializeQueryArgs, 
  copyWithStructuralSharing, 
  retry 
} from '@reduxjs/toolkit/query';

// Custom query argument serialization
const customApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  serializeQueryArgs: ({ queryArgs, endpointDefinition, endpointName }) => {
    // Use default serialization with custom prefix
    const defaultKey = defaultSerializeQueryArgs(queryArgs, endpointDefinition, endpointName);
    return `custom:${defaultKey}`;
  },
  endpoints: (builder) => ({
    getUser: builder.query<User, { userId: string; include?: string[] }>({
      query: ({ userId, include = [] }) => ({
        url: `users/${userId}`,
        params: { include: include.join(',') }
      })
    })
  })
});

// Enhanced base query with retry logic
const resilientBaseQuery = retry(
  fetchBaseQuery({ baseUrl: '/api' }),
  {
    maxRetries: 3,
    retryCondition: (error, args, extraOptions) => {
      // Retry on network errors and 5xx server errors
      return error.status >= 500 || error.name === 'NetworkError';
    },
    backoff: (attempt, maxRetries) => {
      // Exponential backoff: 1s, 2s, 4s
      return Math.min(1000 * (2 ** attempt), 10000);
    }
  }
);

const apiWithRetry = createApi({
  baseQuery: resilientBaseQuery,
  endpoints: (builder) => ({
    getCriticalData: builder.query<Data, void>({
      query: () => 'critical-data'
    })
  })
});

// Structural sharing in transformResponse
const optimizedApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getOptimizedData: builder.query<ProcessedData, void>({
      query: () => 'data',
      transformResponse: (response: RawData, meta, arg) => {
        const processed = processData(response);
        // copyWithStructuralSharing is used internally by RTK Query
        // but can be useful for custom transform logic
        return {
          ...processed,
          metadata: {
            fetchedAt: Date.now(),
            version: response.version
          }
        };
      }
    })
  })
});

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