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:
jtricerolph 2026-07-20 14:37:36 +00:00
parent cae411eae7
commit 1c411e402e
19809 changed files with 1962608 additions and 97 deletions

View file

@ -0,0 +1,269 @@
---
name: evolve-and-diagnose-redux-apps/debug-redux-toolkit-apps
description: >
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.
type: lifecycle
library: "@reduxjs/toolkit"
library_version: "2.11.2"
requires:
- build-modern-redux-apps/redux-dataflow
sources:
- "reduxjs/redux:docs/style-guide/style-guide.md"
- "reduxjs/redux:docs/tutorials/essentials/part-5-async-logic.md"
- "reduxjs/redux:docs/tutorials/essentials/part-8-rtk-query-advanced.md"
- "reduxjs/redux-toolkit:docs/usage/usage-guide.md"
---
# Debug Redux Toolkit Apps
## Setup
```ts
import { configureStore, createAsyncThunk, createSlice } from '@reduxjs/toolkit'
type Post = { id: string; title: string }
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async () => {
const response = await fetch('/api/posts')
return (await response.json()) as Post[]
},
{
condition(_arg, { getState }) {
const state = getState() as RootState
return state.posts.status === 'idle'
},
},
)
const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [] as Post[],
status: 'idle' as 'idle' | 'pending' | 'succeeded' | 'failed',
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = 'pending'
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
state.items = action.payload
})
},
})
export const store = configureStore({
reducer: {
posts: postsSlice.reducer,
},
})
type RootState = ReturnType<typeof store.getState>
```
## Core Patterns
### Debug in order: action -> reducer -> selector -> render
```ts
const selectPosts = (state: RootState) => state.posts.items
const selectPostsStatus = (state: RootState) => state.posts.status
store.dispatch(fetchPosts())
console.log(selectPostsStatus(store.getState()))
console.log(selectPosts(store.getState()))
```
If a component looks wrong, first verify the action fired, then the reducer state, then the selector result, then the render boundary.
### Narrow subscriptions at the usage site
```tsx
import { useAppSelector } from '../../app/hooks'
export function PostsList() {
const posts = useAppSelector((state) => state.posts.items)
const status = useAppSelector((state) => state.posts.status)
return (
<div>
<div>{status}</div>
<div>{posts.length}</div>
</div>
)
}
```
React-Redux behaves best when components select only the values they render and do it as close to usage as possible.
### Interpret RTK Query invalidation correctly
```ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
type Post = { id: string; title: string }
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
tagTypes: ['Post'],
endpoints: (build) => ({
getPosts: build.query<Post[], void>({
query: () => 'posts',
providesTags: ['Post'],
}),
updatePost: build.mutation<Post, Pick<Post, 'id' | 'title'>>({
query: ({ id, title }) => ({
url: `posts/${id}`,
method: 'PATCH',
body: { title },
}),
invalidatesTags: ['Post'],
}),
}),
})
```
If invalidation did not visibly refetch, check whether anything was still subscribed to that cache entry.
## Common Mistakes
### HIGH Dispatching fetch thunks from effects without a thunk-level guard
Wrong:
```tsx
import { useEffect } from 'react'
import { useAppDispatch, useAppSelector } from '../../app/hooks'
function PostsPage() {
const dispatch = useAppDispatch()
const postStatus = useAppSelector((state) => state.posts.status)
useEffect(() => {
if (postStatus === 'idle') {
dispatch(fetchPosts())
}
}, [dispatch, postStatus])
return null
}
```
Correct:
```ts
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async () => {
const response = await fetch('/api/posts')
return (await response.json()) as Post[]
},
{
condition(_arg, { getState }) {
const state = getState() as RootState
return state.posts.status === 'idle'
},
},
)
```
React StrictMode can run effects twice in development, so the guard belongs in the thunk as well as the component.
Source: reduxjs/redux:docs/tutorials/essentials/part-5-async-logic.md
### HIGH Ignoring serializable-state warnings
Wrong:
```ts
const initialState = {
lastSeen: new Date(),
pendingIds: new Set<string>(),
}
```
Correct:
```ts
const initialState = {
lastSeenIso: new Date().toISOString(),
pendingIds: [] as string[],
}
```
Non-serializable values break DevTools, replay, persistence, and equality assumptions in subtle ways.
Source: reduxjs/redux:docs/style-guide/style-guide.md
### HIGH Selecting broad state in parents and threading props
Wrong:
```tsx
import { useAppSelector } from '../../app/hooks'
function PostsPage() {
const postsState = useAppSelector((state) => state.posts)
return <PostsList items={postsState.items} status={postsState.status} />
}
```
Correct:
```tsx
import { useAppSelector } from '../../app/hooks'
function PostsList() {
const items = useAppSelector((state) => state.posts.items)
const status = useAppSelector((state) => state.posts.status)
return (
<div>
<div>{status}</div>
<div>{items.length}</div>
</div>
)
}
```
Selecting whole slices high in the tree widens the subscription surface and pushes rerenders through props.
Source: reduxjs/redux:docs/style-guide/style-guide.md
### MEDIUM Returning unstable objects from query selection logic
Wrong:
```tsx
import { api } from '../../services/api'
const result = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data = [] }) => ({
posts: [...data],
}),
})
```
Correct:
```tsx
import { api } from '../../services/api'
const result = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data = [] }) => ({
posts: data,
}),
})
```
New object and array references defeat memoization and make components rerender even when the underlying cached data did not change.
Source: reduxjs/redux:docs/tutorials/essentials/part-8-rtk-query-advanced.md

View file

@ -0,0 +1,226 @@
---
name: evolve-and-diagnose-redux-apps/migrate-to-modern-redux
description: >
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.
type: lifecycle
library: "@reduxjs/toolkit"
library_version: "2.11.2"
requires:
- build-modern-redux-apps/modern-redux
sources:
- "reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx"
- "reduxjs/redux-toolkit:docs/usage/migrating-rtk-2.md"
- "reduxjs/redux-toolkit:packages/rtk-codemods/README.md"
- "reduxjs/redux:docs/style-guide/style-guide.md"
---
# Migrate To Modern Redux
## Setup
```ts
// before
import { applyMiddleware, combineReducers, createStore } from 'redux'
import thunk from 'redux-thunk'
const postsReducer = (state = [] as { id: string; title: string }[]) => state
const usersReducer = (state = [] as { id: string; name: string }[]) => state
const rootReducer = combineReducers({
posts: postsReducer,
users: usersReducer,
})
export const legacyStore = createStore(rootReducer, applyMiddleware(thunk))
// after
import { configureStore } from '@reduxjs/toolkit'
const postsReducer = (state = [] as { id: string; title: string }[]) => state
const usersReducer = (state = [] as { id: string; name: string }[]) => state
export const store = configureStore({
reducer: {
posts: postsReducer,
users: usersReducer,
},
})
```
## Core Patterns
### Replace the store setup first
```ts
import { configureStore } from '@reduxjs/toolkit'
const postsReducer = (state = [] as { id: string; title: string }[]) => state
const usersReducer = (state = [] as { id: string; name: string }[]) => state
export const store = configureStore({
reducer: {
posts: postsReducer,
users: usersReducer,
},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
```
This is the one migration step that can happen immediately while old reducers continue to work.
### Migrate reducers as you touch them
```ts
import { createSlice } from '@reduxjs/toolkit'
type TodosState = {
items: { id: string; text: string; completed: boolean }[]
}
const initialState: TodosState = {
items: [],
}
export const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action: { payload: { id: string; text: string } }) {
state.items.push({ ...action.payload, completed: false })
},
todoToggled(state, action: { payload: { id: string } }) {
const todo = state.items.find((item) => item.id === action.payload.id)
if (todo) {
todo.completed = !todo.completed
}
},
},
})
```
Once a reducer needs editing, migrate that reducer instead of adding more legacy code to it.
### Use codemods for mechanical updates
```bash
npx @reduxjs/rtk-codemods createSliceBuilder src/features/posts/postsSlice.ts
npx @reduxjs/rtk-codemods createReducerBuilder src/features/posts/postsReducer.ts
```
Use codemods for repetitive RTK API migrations, then review the result and finish the semantic cleanup by hand.
### Replace legacy server-data stacks with RTK Query
```ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
type Todo = { id: string; text: string }
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
endpoints: (build) => ({
getTodos: build.query<Todo[], void>({
query: () => 'todos',
}),
}),
})
```
When the old code is just request status plus fetched data, migrate toward RTK Query instead of carrying the thunk stack forward forever.
## Common Mistakes
### HIGH Attempting a big-bang rewrite
Wrong:
```ts
// Replace every reducer, every connected component, and every async flow
// in one branch before shipping anything.
```
Correct:
```ts
// 1) Switch createStore to configureStore
// 2) Migrate one touched reducer to createSlice
// 3) Convert touched connected components to hooks
// 4) Repeat without introducing new legacy Redux code
```
Modern Redux migration is incremental, but once the store is modernized new work should stop adding legacy patterns.
Source: reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx
### CRITICAL Carrying removed RTK 2 config forms forward
Wrong:
```ts
configureStore({
reducer,
middleware: [logger],
})
```
Correct:
```ts
configureStore({
reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(logger),
})
```
RTK 2 removed array middleware configuration and other older builder forms that agents trained on RTK 1.x still emit.
Source: reduxjs/redux-toolkit:docs/usage/migrating-rtk-2.md
### HIGH Preserving hand-written fetch state by default
Wrong:
```ts
import { createAsyncThunk } from '@reduxjs/toolkit'
type Todo = { id: string; text: string }
const initialState = {
items: [] as Todo[],
status: 'idle' as 'idle' | 'pending' | 'failed',
}
export const fetchTodos = createAsyncThunk('todos/fetch', async () => {
const response = await fetch('/api/todos')
return (await response.json()) as Todo[]
})
```
Correct:
```ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
type Todo = { id: string; text: string }
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
endpoints: (build) => ({
getTodos: build.query<Todo[], void>({
query: () => 'todos',
}),
}),
})
```
If the feature is really server cache, keep the migration moving toward RTK Query instead of rebuilding the old loading-flag architecture in new APIs.
Source: reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx