web-state-redux-toolkit-b00aa9 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-state-redux-toolkit-b00aa9 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: Redux Toolkit earns its place where state is complex enough to want DevTools, middleware, time-travel or normalised entities.configureStoreandcreateSliceare the whole authoring surface; Immer makes reducers read as mutations while staying immutable. RTK 2.0 removed the object form ofextraReducersand the array forms ofmiddlewareandenhancers— all three are callbacks now — andAnyActiongave way toUnknownAction.
Detailed Resources:
configureStore, createSlice, Immer reducers, prepare callbacksuseAppSelector / useAppDispatch via .withTypes()createApi, endpoints, cache tags, invalidationcreateAsyncThunk and its lifecycle actionscreateSelector and memoisationcombineSlices, inline selectors, buildCreateSlice<critical_requirements>
Build the store with `configureStore`. It wires DevTools, the thunk middleware and the development-only mutation and serialisability checks, none of which createStore does.
Write reducers with `createSlice`. The action types and creators come from the reducer names, so a typo becomes a compile error rather than an action nothing handles.
Define `useAppSelector` and `useAppDispatch` once, in their own file. The plain hooks do not know RootState and do not know the dispatch accepts thunks; defining them alongside the store creates an import cycle.
Register the RTK Query middleware when you register its reducer. Without it the cache never populates, polling never fires and invalidation never runs — all silently, since the queries still resolve.
Compute derived values in selectors rather than storing them. A count kept in state has to be recalculated by every reducer that can change it, and the one that forgets is the bug.
</critical_requirements>
Auto-detection: configureStore, createSlice, createAsyncThunk, createEntityAdapter, createSelector, createApi, PayloadAction, useSelector, useDispatch, .withTypes(), extraReducers, combineSlices, buildCreateSlice, UnknownAction, @reduxjs/toolkit
Applies to:
RootState / AppDispatch inference chainHandled elsewhere:
<philosophy>
One store, changed only by pure reducers, in response to actions that describe what happened. That constraint is what buys the DevTools timeline, replayable sessions and a state change you can point at.
RTK's contribution is removing the cost of it. createSlice derives the action types and creators from the reducer names, so the three-file dance of constants, creators and a switch statement collapses to one object. Immer lets a reducer read as a mutation while producing a new state, so the spread chains that made deep updates error-prone go away.
The overhead that remains is conceptual rather than syntactic: an action indirection between an event and a state change. Where state is small and flat, that indirection buys nothing and something lighter fits better.
</philosophy>
<decision_framework>
Is it a straightforward request against an endpoint?
├─ YES → RTK Query — caching, invalidation and generated hooks come with it
└─ NO → Is it a multi-step flow that reads state as it goes?
├─ YES → createAsyncThunk — sequential calls, conditional logic, upload progress
└─ NO → Is it a side effect reacting to actions?
└─ YES → middleware — logging, analytics, persistenceDo the items have unique ids?
├─ NO → a plain object or array in the slice
└─ YES → Are they looked up or updated individually?
├─ YES → createEntityAdapter — O(1) by id, CRUD reducers, memoised selectors
└─ NO → an array in the slice is simpler and reads in order for free</decision_framework>
<patterns>
The store's own type is the source for RootState and AppDispatch, so nothing is typed by hand.
export const store = configureStore({
reducer: { todos: todosReducer, [apiSlice.reducerPath]: apiSlice.reducer },
middleware: (getDefault) => getDefault().concat(apiSlice.middleware),
});
setupListeners(store.dispatch); // refetch on focus and reconnect
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;Full code: examples/core.md
State, reducers and action creators in one place. The "mutations" run through Immer.
const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.items.find((t) => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
},
},
});A prepare callback covers an action creator that has to build its payload — generating an id, stamping a time.
Full code: examples/core.md
Defined once, against the store's inferred types.
// store/hooks.ts
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();.withTypes() needs React Redux 9.1 or later. These live beside the store rather than in it, or the store imports the hooks that import the store.
Full code: examples/typed-hooks.md
An API slice declares endpoints and the tags that connect a mutation to the queries it invalidates.
export const apiSlice = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
tagTypes: ["Todo"],
endpoints: (build) => ({
getTodos: build.query<Todo[], void>({
query: () => "/todos",
providesTags: ["Todo"],
}),
addTodo: build.mutation<Todo, NewTodo>({
query: (body) => ({ url: "/todos", method: "POST", body }),
invalidatesTags: ["Todo"],
}),
}),
});Full code: examples/rtk-query.md
Normalised storage — an ids array and an entities map — plus the reducers and selectors that go with it.
const usersAdapter = createEntityAdapter<User, string>({
sortComparer: (a, b) => a.name.localeCompare(b.name),
});
const usersSlice = createSlice({
name: "users",
initialState: usersAdapter.getInitialState(),
reducers: {
userAdded: usersAdapter.addOne,
userUpdated: usersAdapter.updateOne,
},
});Full code: examples/entity-adapters.md
createAsyncThunk dispatches pending, fulfilled and rejected itself; the slice handles them in extraReducers.
const fetchUser = createAsyncThunk(
"users/fetch",
async (id: string, { rejectWithValue }) => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return rejectWithValue("Not found");
return res.json();
},
);rejectWithValue is what makes the failure payload typed instead of a serialised error.
Full code: examples/async-thunks.md
A selector deriving a new array or object memoises, or every render sees a new reference.
const selectActiveTodos = createSelector(
[(state: RootState) => state.todos.items],
(items) => items.filter((t) => !t.completed),
);Full code: examples/selectors.md
The middleware option is a callback so the defaults are kept and extended rather than replaced.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(analyticsMiddleware);Full code: examples/middleware.md
</patterns>
<red_flags>
Breaks at runtime:
refetchOnFocus and refetchOnReconnect are configured and inert.createSlice and createReducer. The same syntax in a thunk mutates the real state object.Surprising behaviour:
updateOne and updateMany merge shallowly, so a changes object naming a nested field replaces the whole nested object and drops its siblings."User" and "user" are two tags, and a mutation invalidating one leaves the other's queries alone.getDefaultMiddleware is a function to call, not a value to spread; the callback receives it and the result is what gets concatenated.createAsyncThunk dispatches its own lifecycle actions — dispatching pending by hand runs the handler twice.items.filter(...) unmemoised returns a new array every call, so a connected component re-renders on every action.RootState is inferred from store.getState, so a slice whose state type is loose quietly loosens the type every selector is checked against.AnyAction with UnknownAction, which does not let you read action.type until isAction() has narrowed it — deliberately, since middleware receives anything.</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.