Help us improve
Share bugs, ideas, or general feedback.
From react-rtk
Redux Toolkit and RTK Query patterns. Use when editing slice files, API definitions, store configuration, selectors, or any file importing from @reduxjs/toolkit.
npx claudepluginhub surfertas/claude-react-rtk --plugin react-rtkHow this skill is triggered — by the user, by Claude, or both
Slash command
/react-rtk:redux-rtkThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
```typescript
Configures RTK Query with createApi and fetchBaseQuery for automatic caching, deduplication, and loading state management. Useful when adding data fetching or replacing hand-written thunks in a Redux Toolkit app.
Guides React state management patterns with Redux Toolkit, Zustand, Jotai, React Query for local, global, server state, and library selection.
Implements React state management patterns with Redux Toolkit, Zustand, Jotai, React Query for global state, server state, optimistic updates, and library selection.
Share bugs, ideas, or general feedback.
// store/api/baseApi.ts — ONE base API per backend
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) headers.set('Authorization', `Bearer ${token}`);
return headers;
},
}),
tagTypes: ['User', 'Post', 'Comment'], // ALL tag types here
endpoints: () => ({}), // Injected from feature files
});
// store/api/usersApi.ts — inject endpoints per feature
export const usersApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query<User[], { role?: string }>({
query: (params) => ({ url: 'users', params }),
providesTags: (result) =>
result
? [...result.map(({ id }) => ({ type: 'User' as const, id })), 'User']
: ['User'],
}),
}),
});
export const { useGetUsersQuery } = usersApi;
// store/slices/uiSlice.ts
interface UiState {
sidebarCollapsed: boolean;
theme: 'light' | 'dark' | 'system';
}
const uiSlice = createSlice({
name: 'ui',
initialState: { sidebarCollapsed: false, theme: 'system' } as UiState,
reducers: {
toggleSidebar: (state) => { state.sidebarCollapsed = !state.sidebarCollapsed; },
setTheme: (state, action: PayloadAction<UiState['theme']>) => { state.theme = action.payload; },
},
});
// store/selectors/userSelectors.ts
import { createSelector } from '@reduxjs/toolkit';
const selectUsersState = (state: RootState) => state.users;
export const selectActiveUsers = createSelector(
selectUsersState,
(users) => users.ids.filter(id => users.entities[id]?.status === 'active')
);
// store/hooks.ts — USE THESE, not raw useDispatch/useSelector
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
// server-parallel-fetching: independent fetches in RSC must run in parallel
// BAD: parent fetches sequentially, children wait
async function Page() {
const user = await getUser(); // 200ms
const posts = await getPosts(); // 300ms — total: 500ms
return <Dashboard user={user} posts={posts} />;
}
// GOOD: sibling components fetch independently, React streams both
async function Page() {
return (
<>
<Suspense fallback={<UserSkeleton />}>
<UserSection /> {/* fetches getUser() internally */}
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<PostsSection /> {/* fetches getPosts() internally */}
</Suspense>
</>
); // total: max(200, 300) = 300ms
}
// server-serialization: minimize Server → Client data transfer
// BAD: passes entire object with unused fields
<ClientComponent user={fullUserObject} />
// GOOD: select only needed fields
<ClientComponent userName={user.name} userAvatar={user.avatar} />
// server-cache-react: React.cache() for per-request deduplication in RSC
import { cache } from 'react';
const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
// Multiple components call getUser(id) — only ONE query runs per request
// server-cache-lru: cross-request cache for expensive computations
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, ComputedResult>({
max: 500,
ttl: 1000 * 60 * 5, // 5 minutes
});
export async function getExpensiveData(key: string) {
const cached = cache.get(key);
if (cached) return cached;
const result = await computeExpensiveData(key);
cache.set(key, result);
return result;
}
// client-swr-dedup: when NOT using RTK Query, use SWR for deduplication
// SWR automatically deduplicates identical requests across components
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
function UserName({ id }: { id: string }) {
const { data } = useSWR(`/api/users/${id}`, fetcher);
return <span>{data?.name}</span>;
}
// If 5 components call useSWR('/api/users/1'), only 1 request fires
// NOTE: If RTK Query is in the project, use RTK Query instead (Iron Law 15)
For detailed patterns, see references/ directory.