Add settings page for managing forecasting API key and URL via UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cae411eae7
commit
1c411e402e
19809 changed files with 1962608 additions and 97 deletions
364
frontend/node_modules/@reduxjs/toolkit/skills/model-redux-state/build-slices-and-selectors/SKILL.md
generated
vendored
Normal file
364
frontend/node_modules/@reduxjs/toolkit/skills/model-redux-state/build-slices-and-selectors/SKILL.md
generated
vendored
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
---
|
||||
name: model-redux-state/build-slices-and-selectors
|
||||
description: >
|
||||
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.
|
||||
type: core
|
||||
library: "@reduxjs/toolkit"
|
||||
library_version: "2.11.2"
|
||||
requires:
|
||||
- model-redux-state/design-state-ownership
|
||||
sources:
|
||||
- "reduxjs/redux-toolkit:docs/api/createSlice.mdx"
|
||||
- "reduxjs/redux-toolkit:docs/api/combineSlices.mdx"
|
||||
- "reduxjs/redux-toolkit:docs/api/createEntityAdapter.mdx"
|
||||
- "reduxjs/redux-toolkit:docs/usage/immer-reducers.md"
|
||||
- "reduxjs/redux-toolkit:docs/usage/migrating-rtk-2.md"
|
||||
- "reduxjs/redux:docs/style-guide/style-guide.md"
|
||||
---
|
||||
|
||||
# Build Slices And Selectors
|
||||
|
||||
## Setup
|
||||
|
||||
```ts
|
||||
// file: src/app/createAppSlice.ts
|
||||
import { asyncThunkCreator, buildCreateSlice } from '@reduxjs/toolkit'
|
||||
|
||||
export const createAppSlice = buildCreateSlice({
|
||||
creators: { asyncThunk: asyncThunkCreator },
|
||||
})
|
||||
|
||||
// file: src/features/posts/postsSlice.ts
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import { createAppSlice } from '../../app/createAppSlice'
|
||||
|
||||
type PostsState = {
|
||||
items: { id: string; title: string; published: boolean }[]
|
||||
status: 'idle' | 'pending' | 'succeeded' | 'failed'
|
||||
}
|
||||
|
||||
const initialState: PostsState = {
|
||||
items: [],
|
||||
status: 'idle',
|
||||
}
|
||||
|
||||
export const postsSlice = createAppSlice({
|
||||
name: 'posts',
|
||||
initialState,
|
||||
reducers: (create) => ({
|
||||
postAdded: create.reducer<{ id: string; title: string }>((state, action) => {
|
||||
state.items.push({ ...action.payload, published: false })
|
||||
}),
|
||||
fetchPosts: create.asyncThunk(
|
||||
async () => {
|
||||
const response = await fetch('/api/posts')
|
||||
return (await response.json()) as { id: string; title: string; published: boolean }[]
|
||||
},
|
||||
{
|
||||
pending: (state) => {
|
||||
state.status = 'pending'
|
||||
},
|
||||
fulfilled: (state, action) => {
|
||||
state.status = 'succeeded'
|
||||
state.items = action.payload
|
||||
},
|
||||
rejected: (state) => {
|
||||
state.status = 'failed'
|
||||
},
|
||||
},
|
||||
),
|
||||
}),
|
||||
selectors: {
|
||||
selectPosts: (state) => state.items,
|
||||
selectPublishedPosts: createSelector(
|
||||
[(state: PostsState) => state.items],
|
||||
(items) => items.filter((post) => post.published),
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const { postAdded, fetchPosts } = postsSlice.actions
|
||||
export const { selectPosts, selectPublishedPosts } = postsSlice.selectors
|
||||
```
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### Use mutating logic inside slice reducers
|
||||
|
||||
```ts
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
const todosSlice = createSlice({
|
||||
name: 'todos',
|
||||
initialState: [] as { id: string; text: string; done: boolean }[],
|
||||
reducers: {
|
||||
todoAdded(state, action: { payload: { id: string; text: string } }) {
|
||||
state.push({ ...action.payload, done: false })
|
||||
},
|
||||
todoToggled(state, action: { payload: { id: string } }) {
|
||||
const todo = state.find((item) => item.id === action.payload.id)
|
||||
if (todo) {
|
||||
todo.done = !todo.done
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Immer is the default inside `createSlice`; write the reducer logic directly instead of copying arrays and objects by hand.
|
||||
|
||||
### Define selectors in the slice when they belong to the slice
|
||||
|
||||
```ts
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
const counterSlice = createSlice({
|
||||
name: 'counter',
|
||||
initialState: { value: 0 },
|
||||
reducers: {
|
||||
increment(state) {
|
||||
state.value += 1
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
selectValue: (state) => state.value,
|
||||
selectIsPositive: (state) => state.value > 0,
|
||||
},
|
||||
})
|
||||
|
||||
const { selectValue, selectIsPositive } = counterSlice.selectors
|
||||
```
|
||||
|
||||
Slice selectors keep state-location knowledge next to the slice.
|
||||
|
||||
### Use `create.asyncThunk` when the async lifecycle belongs to the slice
|
||||
|
||||
```ts
|
||||
import { asyncThunkCreator, buildCreateSlice } from '@reduxjs/toolkit'
|
||||
|
||||
const createAppSlice = buildCreateSlice({
|
||||
creators: { asyncThunk: asyncThunkCreator },
|
||||
})
|
||||
|
||||
const usersSlice = createAppSlice({
|
||||
name: 'users',
|
||||
initialState: { items: [] as { id: string; name: string }[], status: 'idle' as 'idle' | 'pending' | 'failed' },
|
||||
reducers: (create) => ({
|
||||
fetchUsers: create.asyncThunk(
|
||||
async () => {
|
||||
const response = await fetch('/api/users')
|
||||
return (await response.json()) as { id: string; name: string }[]
|
||||
},
|
||||
{
|
||||
pending: (state) => {
|
||||
state.status = 'pending'
|
||||
},
|
||||
fulfilled: (state, action) => {
|
||||
state.status = 'idle'
|
||||
state.items = action.payload
|
||||
},
|
||||
rejected: (state) => {
|
||||
state.status = 'failed'
|
||||
},
|
||||
},
|
||||
),
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
Use this when the async lifecycle handlers naturally live with the slice; otherwise regular `createAsyncThunk` is still fine.
|
||||
|
||||
### Use entity adapters and lazy injection for scalable slices
|
||||
|
||||
```ts
|
||||
import {
|
||||
combineSlices,
|
||||
createEntityAdapter,
|
||||
createSlice,
|
||||
} from '@reduxjs/toolkit'
|
||||
|
||||
type Book = { bookId: string; title: string }
|
||||
|
||||
const booksAdapter = createEntityAdapter<Book>({
|
||||
selectId: (book) => book.bookId,
|
||||
})
|
||||
|
||||
const booksSlice = createSlice({
|
||||
name: 'books',
|
||||
initialState: booksAdapter.getInitialState(),
|
||||
reducers: {
|
||||
booksReceived: booksAdapter.setAll,
|
||||
},
|
||||
})
|
||||
|
||||
export interface LazyLoadedSlices {}
|
||||
|
||||
export const rootReducer =
|
||||
combineSlices().withLazyLoadedSlices<LazyLoadedSlices>()
|
||||
|
||||
declare module './rootReducer' {
|
||||
export interface LazyLoadedSlices {}
|
||||
}
|
||||
|
||||
const injectedBooksSlice = booksSlice.injectInto(rootReducer)
|
||||
|
||||
const selectors = booksAdapter.getSelectors(
|
||||
(state: ReturnType<typeof rootReducer.selector.original>) =>
|
||||
injectedBooksSlice.selectSlice(state),
|
||||
)
|
||||
```
|
||||
|
||||
Entity adapters standardize normalized collections, and `injectInto` lets a slice stay aware of its injected location.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### CRITICAL Using mutating logic outside slice reducers
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
type Todo = { id: string; text: string }
|
||||
|
||||
export function addTodo(todos: Todo[], todo: Todo) {
|
||||
todos.push(todo)
|
||||
return todos
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
type Todo = { id: string; text: string }
|
||||
|
||||
const todosSlice = createSlice({
|
||||
name: 'todos',
|
||||
initialState: [] as Todo[],
|
||||
reducers: {
|
||||
todoAdded(state, action: { payload: Todo }) {
|
||||
state.push(action.payload)
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Mutation syntax is only safe inside Immer-backed reducer contexts such as `createSlice` and `createReducer`.
|
||||
|
||||
Source: reduxjs/redux-toolkit:docs/usage/immer-reducers.md
|
||||
|
||||
### HIGH Writing hand-written switch reducers as the default
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
export default function todosReducer(state = initialState, action: { type: string; payload?: Todo }) {
|
||||
switch (action.type) {
|
||||
case 'todos/todoAdded':
|
||||
return state.concat(action.payload as Todo)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
const todosSlice = createSlice({
|
||||
name: 'todos',
|
||||
initialState,
|
||||
reducers: {
|
||||
todoAdded(state, action: { payload: Todo }) {
|
||||
state.push(action.payload)
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Hand-written reducers are an escape hatch for proven bottlenecks, not the normal thing an agent should generate in RTK code.
|
||||
|
||||
Source: reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx
|
||||
|
||||
### HIGH Writing RTK 1.x object syntax for `extraReducers`
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
const initialState = { items: [] as { id: string; title: string }[] }
|
||||
|
||||
const fetchPosts = createAsyncThunk('posts/fetch', async () => {
|
||||
const response = await fetch('/api/posts')
|
||||
return (await response.json()) as { id: string; title: string }[]
|
||||
})
|
||||
|
||||
const postsSlice = createSlice({
|
||||
name: 'posts',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: {
|
||||
[fetchPosts.fulfilled.type]: (state, action) => {
|
||||
state.items = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
const initialState = { items: [] as { id: string; title: string }[] }
|
||||
|
||||
const fetchPosts = createAsyncThunk('posts/fetch', async () => {
|
||||
const response = await fetch('/api/posts')
|
||||
return (await response.json()) as { id: string; title: string }[]
|
||||
})
|
||||
|
||||
const postsSlice = createSlice({
|
||||
name: 'posts',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(fetchPosts.fulfilled, (state, action) => {
|
||||
state.items = action.payload
|
||||
})
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
RTK 2 removed the object form; agents trained on RTK 1.x still generate it.
|
||||
|
||||
Source: reduxjs/redux-toolkit:docs/usage/migrating-rtk-2.md
|
||||
|
||||
### HIGH Assuming `entity.id` exists for every collection
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
type Book = { bookId: string; title: string }
|
||||
|
||||
const booksAdapter = createEntityAdapter<Book>()
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
type Book = { bookId: string; title: string }
|
||||
|
||||
const booksAdapter = createEntityAdapter<Book>({
|
||||
selectId: (book) => book.bookId,
|
||||
})
|
||||
```
|
||||
|
||||
Adapters default to `entity.id`; collections keyed by another field must provide `selectId`.
|
||||
|
||||
Source: reduxjs/redux-toolkit:docs/api/createEntityAdapter.mdx
|
||||
|
||||
## References
|
||||
|
||||
- [Slice selectors, async creators, and lazy injection details](references/slice-patterns.md)
|
||||
59
frontend/node_modules/@reduxjs/toolkit/skills/model-redux-state/build-slices-and-selectors/references/slice-patterns.md
generated
vendored
Normal file
59
frontend/node_modules/@reduxjs/toolkit/skills/model-redux-state/build-slices-and-selectors/references/slice-patterns.md
generated
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Slice Patterns
|
||||
|
||||
## `getSelectors` for alternate mounting points
|
||||
|
||||
```ts
|
||||
const counterSlice = createSlice({
|
||||
name: 'counter',
|
||||
initialState: { value: 0 },
|
||||
reducers: {},
|
||||
selectors: {
|
||||
selectValue: (state) => state.value,
|
||||
},
|
||||
})
|
||||
|
||||
export const { selectValue } = counterSlice.getSelectors(
|
||||
(state: { customCounter: { value: number } }) => state.customCounter,
|
||||
)
|
||||
```
|
||||
|
||||
Use `getSelectors` when the slice is not mounted at its default `reducerPath`.
|
||||
|
||||
## `withLazyLoadedSlices` and `injectInto`
|
||||
|
||||
```ts
|
||||
import { combineSlices, createSlice, type WithSlice } from '@reduxjs/toolkit'
|
||||
|
||||
const staticSlice = createSlice({
|
||||
name: 'static',
|
||||
initialState: { ready: true },
|
||||
reducers: {},
|
||||
})
|
||||
|
||||
export interface LazyLoadedSlices {}
|
||||
|
||||
export const rootReducer =
|
||||
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()
|
||||
|
||||
const lazySlice = createSlice({
|
||||
name: 'lazy',
|
||||
initialState: { value: 0 },
|
||||
reducers: {
|
||||
increment(state) {
|
||||
state.value += 1
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
declare module '../rootReducer' {
|
||||
export interface LazyLoadedSlices extends WithSlice<typeof lazySlice> {}
|
||||
}
|
||||
|
||||
export const injectedLazySlice = lazySlice.injectInto(rootReducer)
|
||||
```
|
||||
|
||||
This keeps RootState types aware of reducers that will be injected later.
|
||||
|
||||
## Selector caveat
|
||||
|
||||
If a selector depends on caller-specific arguments and must be memoized per caller, prefer a selector factory outside `createSlice.selectors`. `createSlice.selectors` gives you one selector instance, not a selector factory.
|
||||
322
frontend/node_modules/@reduxjs/toolkit/skills/model-redux-state/design-state-ownership/SKILL.md
generated
vendored
Normal file
322
frontend/node_modules/@reduxjs/toolkit/skills/model-redux-state/design-state-ownership/SKILL.md
generated
vendored
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
---
|
||||
name: model-redux-state/design-state-ownership
|
||||
description: >
|
||||
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.
|
||||
type: core
|
||||
library: "@reduxjs/toolkit"
|
||||
library_version: "2.11.2"
|
||||
sources:
|
||||
- "reduxjs/redux:docs/style-guide/style-guide.md"
|
||||
- "reduxjs/redux:docs/tutorials/essentials/part-2-app-structure.md"
|
||||
- "reduxjs/redux:docs/tutorials/essentials/part-4-using-data.md"
|
||||
---
|
||||
|
||||
# Design State Ownership
|
||||
|
||||
## Setup
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```tsx
|
||||
import { useAppSelector } from '../../app/hooks'
|
||||
|
||||
const selectDraftTitle = (state: { draft: { title: string } }) => state.draft.title
|
||||
|
||||
const title = useAppSelector(selectDraftTitle)
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
const state = { id: '1', name: 'Lenz' }
|
||||
const action = { payload: { id: '2', name: 'Mark', ignored: true } }
|
||||
|
||||
userLoggedIn(state, action) {
|
||||
return { ...state, ...action.payload }
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
- [State ownership heuristics](references/state-ownership.md)
|
||||
28
frontend/node_modules/@reduxjs/toolkit/skills/model-redux-state/design-state-ownership/references/state-ownership.md
generated
vendored
Normal file
28
frontend/node_modules/@reduxjs/toolkit/skills/model-redux-state/design-state-ownership/references/state-ownership.md
generated
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# State Ownership Heuristics
|
||||
|
||||
## Choose the owner, then the tool
|
||||
|
||||
| Kind of state | Default owner | Typical tool |
|
||||
| --- | --- | --- |
|
||||
| Editable form fields | Component | `useState` |
|
||||
| Shared mutable app data | Redux | Slice state |
|
||||
| Server cache | RTK Query | `createApi` |
|
||||
| URL, pathname, search params | Router | Router APIs plus selector inputs |
|
||||
| Browser-only authority like `localStorage` | External source | Read at boundaries, then dispatch events |
|
||||
|
||||
## Good reasons to move data into Redux
|
||||
|
||||
- Multiple distant parts of the UI need the same mutable data.
|
||||
- You need time-travel debugging or a stable action history.
|
||||
- The reducer should own transitions because they mix old store state with new inputs.
|
||||
|
||||
## Reasons to keep data out of Redux
|
||||
|
||||
- Another system already owns it, such as the router.
|
||||
- It only matters during editing inside one component tree.
|
||||
- It is server cache and RTK Query fits the use case better.
|
||||
|
||||
## Re-evaluate slice size
|
||||
|
||||
- If data is constantly stitched together outside reducers, it may belong closer together.
|
||||
- If unrelated updates keep touching the same slice, split the slice by domain ownership.
|
||||
Loading…
Add table
Add a link
Reference in a new issue