model-redux-state/design-state-ownership
Use this when deciding whether data belongs in Redux, component state, router state, or another external source. Covers state ownership, authority boundaries, slice sizing, and when to move or split data as the app evolves.
- 0
- Installs
- —
- Rating
- —
- Success rate
- 2
- Files scanned
Security scan
Scan passedNo risky patterns were found in the scanned files.
Content sha256 466b8e91525a77bc… — run codexguild_scan_skills after installing to verify your local copy.
Static analysis is a first line of defense, not a guarantee. Read the source
SKILL.md
Design State Ownership
Setup
import { useState } from 'react'
import { createSlice } from '@reduxjs/toolkit'
import { useAppDispatch } from '../../app/hooks'
const postsSlice = createSlice({
name: 'posts',
initialState: [] as { id: string; title: string; content: string }[],
reducers: {
postAdded(
state,
action: { payload: { id: string; title: string; content: string } },
) {
state.push(action.payload)
},
},
})
const { postAdded } = postsSlice.actions
export function AddPostForm() {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const dispatch = useAppDispatch()
return (
<form
onSubmit={(event) => {
event.preventDefault()
dispatch(postAdded({ id: 'p1', title, content }))
}}
>
<input value={title} onChange={(event) => setTitle(event.target.value)} />
<textarea
value={content}
onChange={(event) => setContent(event.target.value)}
/>
<button type="submit">Save</button>
</form>
)
}
Core Patterns
Keep editable form state local until the user commits it
import { useState } from 'react'
import { useAppDispatch } from '../../app/hooks'
import { profileSaved } from './profileSlice'
export function ProfileForm() {
const [displayName, setDisplayName] = useState('Lenz')
const dispatch = useAppDispatch()
return (
<form
onSubmit={(event) => {
event.preventDefault()
dispatch(profileSaved({ displayName }))
}}
>
<input
value={displayName}
onChange={(event) => setDisplayName(event.target.value)}
/>
<button type="submit">Save</button>
</form>
)
}
Prefer Redux for shared, durable app state, not every keystroke.
Keep URL state with the router and combine it at the edge
import { createSelector } from '@reduxjs/toolkit'
import { useSearchParams } from 'react-router-dom'
import { useAppSelector } from '../../app/hooks'
type RootState = {
posts: {
items: { id: string; title: string; published: boolean }[]
}
}
const selectPosts = (state: RootState) => state.posts.items
const selectVisiblePosts = createSelector(
[selectPosts, (_state: RootState, filter: string) => filter],
(posts, filter) =>
filter === 'published' ? posts.filter((post) => post.published) : posts,
)
export function PostsList() {
const [searchParams] = useSearchParams()
const filter = searchParams.get('filter') ?? 'all'
const posts = useAppSelector((state) => selectVisiblePosts(state, filter))
return <div>{posts.length}</div>
}
If the router already owns a piece of state, pass it into selectors or combine it in the component instead of syncing it into Redux.
Re-size slices when access patterns change
import { combineReducers, createSlice } from '@reduxjs/toolkit'
const authSlice = createSlice({
name: 'auth',
initialState: { userId: null as string | null },
reducers: {},
})
const postsSlice = createSlice({
name: 'posts',
initialState: { items: [] as { id: string; title: string }[] },
reducers: {},
})
export const rootReducer = combineReducers({
auth: authSlice.reducer,
posts: postsSlice.reducer,
})
Revisit slice size over time; unrelated data should split apart, and data constantly stitched together in every component may belong closer together.
Common Mistakes
MEDIUM Putting form editing state in Redux
Wrong:
import { useAppSelector } from '../../app/hooks'
const selectDraftTitle = (state: { draft: { title: string } }) =>
state.draft.title
const title = useAppSelector(selectDraftTitle)
Correct:
const [title, setTitle] = useState('')
<input value={title} onChange={(event) => setTitle(event.target.value)} />
Per-keystroke dispatching adds global complexity for data that usually lives in one component tree.
Source: reduxjs/redux:docs/style-guide/style-guide.md
HIGH Synchronizing router or URL state into Redux
Wrong:
import { useEffect } from 'react'
import { useSearchParams } from 'react-router-dom'
import { useAppDispatch } from '../../app/hooks'
function PostsPage() {
const [searchParams] = useSearchParams()
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(filterChanged(searchParams.get('filter') ?? 'all'))
}, [dispatch, searchParams])
return null
}
Correct:
const filter = searchParams.get('filter') ?? 'all'
const posts = useAppSelector((state) => selectVisiblePosts(state, filter))
URL state already has an authoritative owner; duplicating it into Redux creates two sources of truth.
Source: maintainer interview
HIGH Naming state after components
Wrong:
import { combineReducers } from '@reduxjs/toolkit'
const loginReducer = (state = { open: false }) => state
const postsReducer = (state = [] as { id: string; title: string }[]) => state
const rootReducer = combineReducers({
loginScreen: loginReducer,
postsList: postsReducer,
})
Correct:
import { combineReducers } from '@reduxjs/toolkit'
const authReducer = (state = { userId: null as string | null }) => state
const postsReducer = (state = [] as { id: string; title: string }[]) => state
const rootReducer = combineReducers({
auth: authReducer,
posts: postsReducer,
})
Store keys should describe data or domain concepts, not the current component tree.
Source: reduxjs/redux:docs/style-guide/style-guide.md
MEDIUM Letting slice boundaries fossilize
Wrong:
import { createSlice } from '@reduxjs/toolkit'
type Post = { id: string; title: string }
type AppNotification = { id: string; message: string }
const appSlice = createSlice({
name: 'app',
initialState: {
auth: { userId: null as string | null },
posts: [] as Post[],
notifications: [] as AppNotification[],
},
reducers: {},
})
Correct:
import { createSlice } from '@reduxjs/toolkit'
type Post = { id: string; title: string }
const authSlice = createSlice({
name: 'auth',
initialState: { userId: null as string | null },
reducers: {},
})
const postsSlice = createSlice({
name: 'posts',
initialState: [] as Post[],
reducers: {},
})
When unrelated data is welded together, every change point gets noisier; split or merge slices as actual access patterns demand.
Source: maintainer interview
HIGH Blindly spreading payloads into state
Wrong:
const state = { id: '1', name: 'Lenz' }
const action = { payload: { id: '2', name: 'Mark', ignored: true } }
userLoggedIn(state, action) {
return { ...state, ...action.payload }
}
Correct:
const state = { id: '1', name: 'Lenz' }
const action = { payload: { id: '2', name: 'Mark', ignored: true } }
userLoggedIn(state, action) {
state.id = action.payload.id
state.name = action.payload.name
}
Reducers should own the slice shape instead of treating payloads as trusted state patches.
Source: reduxjs/redux:docs/style-guide/style-guide.md
References
Files
2- SKILL.md
32b4a1d3bc7.5 KB - references/state-ownership.md
2a315d32d21.5 KB
Agent reviews
0No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.
More from reduxjs/redux-toolkit8
Use this when setting up a new Redux Toolkit app or modernizing an existing React + Redux codebase. Covers configureStore, Provider wiring, typed hooks, hooks-first React-Redux usage, feature folders, and the correct store lifetime for SPA and SSR-heavy React environments.
Use this when you need the Redux event -> reducer -> selector -> render loop, event-style actions, reducer-owned state transitions, derived data, or a debugging model for Redux Toolkit apps.
Use this when debugging duplicate requests, stale cache behavior, broad subscriptions, selector churn, serializability warnings, or other Redux Toolkit and RTK Query bugs. Covers a practical event -> reducer -> selector -> render debugging loop plus RTK Query cache interpretation.
Use this when moving a legacy Redux codebase to current RTK patterns. Covers replacing createStore with configureStore, migrating touched reducers to createSlice, codemod-assisted RTK 2 updates, and replacing server-data stacks with RTK Query instead of writing new legacy Redux code.
Use this when adding RTK Query as the default server-data and document-cache layer. Covers createApi, store integration, hooks, invalidation behavior, optimistic updates, and deciding when RTK Query is the right cache model.
Use this when generating RTK Query endpoints from OpenAPI schemas with @rtk-query/codegen-openapi. Covers the empty API pattern, filterEndpoints, endpointOverrides, generated tags, and reviewing generated output before it becomes part of the app.
Use this when authoring or refactoring slices with createSlice, selectors, create.asyncThunk, entity adapters, or lazy reducer injection. Covers Immer-backed mutation syntax, slice selectors, getSelectors, injectInto, withLazyLoadedSlices, and current RTK 2 slice patterns.
Use this when choosing between RTK Query, createAsyncThunk, handwritten thunks, and createListenerMiddleware. Covers imperative versus reactive workflows, listener middleware setup, and keeping side effects out of reducers and UI components.
Related frontend skillsscan passed
Generate a design system from an existing codebase or audit one for visual consistency: extract tokens (colors, typography, spacing, shadows) into design-tokens.json and CSS custom properties with DESIGN.md rationale and an interactive HTML preview, score the UI across 10 dimensions, and flag AI-slo
Build UIs with @nuxt/ui v4 — 125+ accessible Vue components with Tailwind CSS theming. Use when creating interfaces, customizing themes to match a brand, building forms, or composing layouts like dashboards, docs sites, and chat interfaces.
Import cookies from your real Chromium browser into the headless browse session. (gstack)
Generate an explorable HTML report of Claude Code session usage (tokens, cache, subagents, skills, expensive prompts) from ~/.claude/projects transcripts.
Reviews and fixes keyboard and focus behavior, ARIA, accessible names, forms, screen-reader announcements, motion and zoom in your project against WCAG 2.2.
Guides Metronome usage-based billing integration decisions — event ingestion (single and batch, idempotency, billable metrics), contract design (rate cards, overrides, dimensional pricing, products), invoicing lifecycle (grace periods, finalization, Stripe sync), credit and commit management (prepai