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,304 @@
---
name: build-modern-redux-apps/modern-redux
description: >
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.
type: lifecycle
library: "@reduxjs/toolkit"
library_version: "2.11.2"
requires:
- build-modern-redux-apps/redux-dataflow
sources:
- "reduxjs/redux-toolkit:docs/tutorials/quick-start.mdx"
- "reduxjs/redux-toolkit:docs/tutorials/typescript.md"
- "reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx"
- "reduxjs/redux-toolkit:docs/usage/nextjs.mdx"
- "reduxjs/redux:docs/style-guide/style-guide.md"
---
# Modern Redux
## Setup
```tsx
// file: src/features/counter/counterSlice.ts
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment(state) {
state.value += 1
},
},
})
export const { increment } = counterSlice.actions
// file: src/app/store.ts
import { configureStore } from '@reduxjs/toolkit'
import { counterSlice } from '../features/counter/counterSlice'
export const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
// file: src/app/hooks.ts
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
// file: src/features/counter/Counter.tsx
import { increment } from './counterSlice'
import { useAppDispatch, useAppSelector } from '../../app/hooks'
export function Counter() {
const value = useAppSelector((state) => state.counter.value)
const dispatch = useAppDispatch()
return <button onClick={() => dispatch(increment())}>{value}</button>
}
// file: src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './app/store'
import { Counter } from './features/counter/Counter'
ReactDOM.createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<Counter />
</Provider>,
)
```
## Core Patterns
### Keep React components on hooks, not wrappers
```tsx
import { postAdded, selectPosts } from './postsSlice'
import { useAppDispatch, useAppSelector } from '../../app/hooks'
export function PostsList() {
const posts = useAppSelector(selectPosts)
const dispatch = useAppDispatch()
return (
<button
onClick={() =>
dispatch(postAdded({ id: 'p2', title: 'Write docs' }))
}
>
{posts.length}
</button>
)
}
```
Hooks are the default React-Redux integration for new code.
### Create the store inside the provider for SSR-heavy React apps
```tsx
// file: src/lib/store.ts
import { configureStore } from '@reduxjs/toolkit'
import { counterSlice } from '../features/counter/counterSlice'
export const makeStore = () =>
configureStore({
reducer: {
counter: counterSlice.reducer,
},
})
export type AppStore = ReturnType<typeof makeStore>
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']
// file: src/app/StoreProvider.tsx
'use client'
import { useState, type ReactNode } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '../lib/store'
export function StoreProvider({ children }: { children: ReactNode }) {
const [store] = useState(makeStore)
return <Provider store={store}>{children}</Provider>
}
```
In SSR-heavy React frameworks, create a store per request and keep that instance stable across renders.
### Keep app wiring in `app/` and feature logic in feature folders
```text
src/
app/
store.ts
hooks.ts
features/
posts/
postsSlice.ts
PostsList.tsx
users/
usersSlice.ts
UsersList.tsx
```
This keeps store wiring centralized and feature logic colocated.
## Common Mistakes
### HIGH Importing the store in React components
Wrong:
```tsx
import { store } from '../../app/store'
export function PostsList() {
const posts = store.getState().posts
return <div>{posts.length}</div>
}
```
Correct:
```tsx
import { useAppSelector } from '../../app/hooks'
export function PostsList() {
const posts = useAppSelector((state) => state.posts)
return <div>{posts.length}</div>
}
```
React components should read through context and hooks; direct store imports are a separate escape hatch for non-React integrations, not the default UI pattern.
Source: reduxjs/redux:docs/style-guide/style-guide.md
### HIGH Defaulting to `connect()` in new React code
Wrong:
```tsx
import { connect } from 'react-redux'
const increment = () => ({ type: 'counter/increment' as const })
const mapState = (state: { counter: { value: number } }) => ({
value: state.counter.value,
})
const mapDispatch = { increment }
function Counter({ value, increment }: ReturnType<typeof mapState> & typeof mapDispatch) {
return <button onClick={() => increment()}>{value}</button>
}
export default connect(mapState, mapDispatch)(Counter)
```
Correct:
```tsx
import { increment } from './counterSlice'
import { useAppDispatch, useAppSelector } from '../../app/hooks'
export function Counter() {
const value = useAppSelector((state) => state.counter.value)
const dispatch = useAppDispatch()
return <button onClick={() => dispatch(increment())}>{value}</button>
}
```
Hooks are the modern default, simpler to type, and the maintainers explicitly want agents to steer new code away from `connect`.
Source: reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx
### HIGH Recreating the store during render in SSR-heavy apps
Wrong:
```tsx
'use client'
import { Provider } from 'react-redux'
import { makeStore } from '../lib/store'
export function StoreProvider({ children }: { children: import('react').ReactNode }) {
const store = makeStore()
return <Provider store={store}>{children}</Provider>
}
```
Correct:
```tsx
'use client'
import { useState } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '../lib/store'
export function StoreProvider({ children }: { children: import('react').ReactNode }) {
const [store] = useState(makeStore)
return <Provider store={store}>{children}</Provider>
}
```
A new store on every render loses client state, while a module singleton can leak across requests on the server.
Source: reduxjs/redux-toolkit:docs/usage/nextjs.mdx
### HIGH Keeping `createStore` boilerplate as the default
Wrong:
```ts
import { applyMiddleware, combineReducers, createStore } from 'redux'
import thunk from 'redux-thunk'
const counterReducer = (state = { value: 0 }) => state
const rootReducer = combineReducers({
counter: counterReducer,
})
export const store = createStore(rootReducer, applyMiddleware(thunk))
```
Correct:
```ts
import { configureStore } from '@reduxjs/toolkit'
const counterReducer = (state = { value: 0 }) => state
export const store = configureStore({
reducer: {
counter: counterReducer,
},
})
```
Manual store setup works, but it throws away RTK's default middleware, dev checks, and the current recommended baseline.
Source: reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx
## References
- [Store lifetime and framework boundaries](references/store-lifetime.md)

View file

@ -0,0 +1,53 @@
# Store Lifetime
## Decision table
| Environment | Default store shape | Why |
| --- | --- | --- |
| Client-only SPA | One module-level singleton store | There is one browser session and no cross-request leakage risk. |
| SSR-heavy React app | `makeStore()` plus provider-local state | Each request needs its own store instance, but that instance must stay stable across client renders. |
| Non-React integration code | Direct store access can be acceptable | This is outside the React context boundary and should stay out of UI components. |
## SPA pattern
```ts
import { configureStore } from '@reduxjs/toolkit'
import { postsSlice } from '../features/posts/postsSlice'
export const store = configureStore({
reducer: {
posts: postsSlice.reducer,
},
})
```
Use this for classic browser SPAs.
## SSR-heavy React pattern
```tsx
// file: src/lib/store.ts
import { configureStore } from '@reduxjs/toolkit'
import { postsSlice } from '../features/posts/postsSlice'
export const makeStore = () =>
configureStore({
reducer: {
posts: postsSlice.reducer,
},
})
// file: src/app/StoreProvider.tsx
'use client'
import { useState, type ReactNode } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '../lib/store'
export function StoreProvider({ children }: { children: ReactNode }) {
const [store] = useState(makeStore)
return <Provider store={store}>{children}</Provider>
}
```
Create the store per request, then keep it stable inside the provider component.

View file

@ -0,0 +1,264 @@
---
name: build-modern-redux-apps/redux-dataflow
description: >
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.
type: core
library: "@reduxjs/toolkit"
library_version: "2.11.2"
sources:
- "reduxjs/redux:docs/tutorials/fundamentals/part-2-concepts-data-flow.md"
- "reduxjs/redux:docs/tutorials/essentials/part-3-data-flow.md"
- "reduxjs/redux:docs/style-guide/style-guide.md"
---
# Redux Dataflow
## Setup
```ts
import { configureStore, createSelector, createSlice } from '@reduxjs/toolkit'
const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [] as { id: string; title: string; published: boolean }[],
filter: 'all' as 'all' | 'published',
},
reducers: {
postAdded(state, action: { payload: { id: string; title: string } }) {
state.items.push({ ...action.payload, published: false })
},
postPublished(state, action: { payload: { id: string } }) {
const post = state.items.find((item) => item.id === action.payload.id)
if (post) {
post.published = true
}
},
filterChanged(state, action: { payload: 'all' | 'published' }) {
state.filter = action.payload
},
},
})
const store = configureStore({
reducer: {
posts: postsSlice.reducer,
},
})
type RootState = ReturnType<typeof store.getState>
const selectPostsState = (state: RootState) => state.posts
const selectVisiblePosts = createSelector([selectPostsState], (postsState) =>
postsState.filter === 'all'
? postsState.items
: postsState.items.filter((post) => post.published),
)
store.dispatch(postsSlice.actions.postAdded({ id: 'p1', title: 'Draft' }))
store.dispatch(postsSlice.actions.postPublished({ id: 'p1' }))
const visiblePosts = selectVisiblePosts(store.getState())
console.log(visiblePosts)
```
## Core Patterns
### Dispatch events, not setters
```ts
const postsSlice = createSlice({
name: 'posts',
initialState: [] as { id: string; title: string }[],
reducers: {
postAdded(state, action: { payload: { id: string; title: string } }) {
state.push(action.payload)
},
postRemoved(state, action: { payload: { id: string } }) {
return state.filter((post) => post.id !== action.payload.id)
},
postUpdated(
state,
action: { payload: { id: string; changes: Partial<{ title: string }> } },
) {
const post = state.find((item) => item.id === action.payload.id)
if (post && action.payload.changes.title) {
post.title = action.payload.changes.title
}
},
},
})
postsSlice.actions.postAdded({ id: 'p1', title: 'Draft' })
```
Event-style actions explain what happened in the UI instead of hiding the transition behind a generic setter.
### Let reducers combine old store data with new outside data
```ts
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'
const postsAdapter = createEntityAdapter<{ id: string; title: string }>()
const postsSlice = createSlice({
name: 'posts',
initialState: postsAdapter.getInitialState(),
reducers: {
postsReceived(state, action: { payload: { id: string; title: string }[] }) {
postsAdapter.upsertMany(state, action.payload)
},
},
})
const incomingPosts = [
{ id: 'p1', title: 'Draft' },
{ id: 'p2', title: 'Published' },
]
postsSlice.actions.postsReceived(incomingPosts)
```
If a transition mixes current store state with new external data, dispatch the new external data and let the reducer own the merge.
### Derive values with selectors instead of storing duplicates
```ts
import { createSelector } from '@reduxjs/toolkit'
const selectPosts = (state: RootState) => state.posts.items
const selectFilter = (state: RootState) => state.posts.filter
export const selectVisiblePosts = createSelector(
[selectPosts, selectFilter],
(posts, filter) =>
filter === 'all'
? posts
: posts.filter((post) => post.published),
)
```
Selectors keep a single source of truth in state while still exposing the shapes the UI needs.
## Common Mistakes
### CRITICAL Mutating selected state outside reducers
Wrong:
```ts
const post = selectPostById(store.getState(), 'p1')
if (post) {
post.title = 'Changed in place'
}
```
Correct:
```ts
store.dispatch(postUpdated({ id: 'p1', changes: { title: 'Changed in place' } }))
```
Objects read from the store are still store state; mutating them outside reducers breaks immutability and stale-render assumptions.
Source: reduxjs/redux:docs/style-guide/style-guide.md
### HIGH Using setter-style actions instead of event-style actions
Wrong:
```ts
const nextPosts = [...selectPosts(store.getState()), { id: 'p2', title: 'Write docs' }]
store.dispatch(setPosts(nextPosts))
```
Correct:
```ts
store.dispatch(postAdded({ id: 'p2', title: 'Write docs' }))
```
Actions should describe events, not ask reducers to blindly replace state with a precomputed value.
Source: reduxjs/redux:docs/style-guide/style-guide.md
### HIGH Combining store state before dispatch
Wrong:
```ts
const currentPosts = selectPosts(store.getState())
const mergedPosts = [
...currentPosts.filter(
(currentPost) =>
!incomingPosts.some((incomingPost) => incomingPost.id === currentPost.id),
),
...incomingPosts,
]
store.dispatch(postsReplaced(mergedPosts))
```
Correct:
```ts
store.dispatch(postsReceived(incomingPosts))
```
If the next state depends on current store state, the reducer should own that combination logic; only authoritative external snapshots should replace state wholesale.
Source: maintainer interview
### HIGH Ignoring current state in async reducers
Wrong:
```ts
builder.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
state.items = action.payload
})
```
Correct:
```ts
builder.addCase(fetchPosts.fulfilled, (state, action) => {
if (state.status === 'pending') {
state.status = 'succeeded'
state.items = action.payload
}
})
```
Reducers that treat every lifecycle action as valid can move the slice into impossible states or let stale requests win.
Source: reduxjs/redux:docs/tutorials/essentials/part-5-async-logic.md
### MEDIUM Storing derived values in state
Wrong:
```ts
const initialState = {
items: [] as Post[],
visiblePosts: [] as Post[],
}
```
Correct:
```ts
const selectVisiblePosts = createSelector(
[selectPosts, selectFilter],
(posts, filter) =>
filter === 'all' ? posts : posts.filter((post) => post.published),
)
```
Derived values drift out of sync quickly; keep the raw state and derive the view shape.
Source: reduxjs/redux:docs/style-guide/style-guide.md