mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-24 17:23:19 +00:00
Compare commits
1 Commits
remove-v4
...
feat/skill
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96b32d3e26 |
97
frontend/.cursor/rules/state-management.mdc
Normal file
97
frontend/.cursor/rules/state-management.mdc
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
globs: **/*.store.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
# State Management: React Query, nuqs, Zustand
|
||||
|
||||
Use the following stack. Do **not** introduce or recommend Redux or React Context for shared/global state.
|
||||
|
||||
## Server state → React Query
|
||||
|
||||
- **Use for:** API responses, time-series data, caching, background refetch, retries, stale/refresh.
|
||||
- **Do not use Redux/Context** to store or mirror data that comes from React Query (e.g. do not dispatch API results into Redux).
|
||||
- Prefer generated React Query hooks from `frontend/src/api/generated` when available.
|
||||
- Keep server state in React Query; expose it via hooks that return the query result (and optionally memoized derived values). Do not duplicate it in Redux or Context.
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD: single source of truth from React Query
|
||||
export function useAppStateHook() {
|
||||
const { data, isError } = useQuery(...)
|
||||
const memoizedConfigs = useMemo(() => ({ ... }), [data?.configs])
|
||||
return { configs: memoizedConfigs, isError, ... }
|
||||
}
|
||||
|
||||
// ❌ BAD: copying React Query result into Redux
|
||||
dispatch({ type: UPDATE_LATEST_VERSION, payload: queryResponse.data })
|
||||
```
|
||||
|
||||
## URL state → nuqs
|
||||
|
||||
- **Use for:** shareable state, filters, time range, selected values, pagination, view state that belongs in the URL.
|
||||
- **Do not use Redux/Context** for state that should be shareable or reflected in the URL.
|
||||
- Use [nuqs](https://nuqs.dev/docs/basic-usage) for typed, type-safe URL search params. Avoid ad-hoc `useSearchParams` encoding/decoding.
|
||||
- Keep URL payload small; respect browser URL length limits (e.g. Chrome ~2k chars). Do not put large datasets or sensitive data in query params.
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD: nuqs for filters / time range / selection
|
||||
const [timeRange, setTimeRange] = useQueryState('timeRange', parseAsString.withDefault('1h'))
|
||||
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
|
||||
|
||||
// ❌ BAD: Redux/Context for shareable or URL-synced state
|
||||
const { timeRange } = useContext(SomeContext)
|
||||
```
|
||||
|
||||
## Client state → Zustand
|
||||
|
||||
- **Use for:** global/client state, cross-component state, feature flags, complex or large client objects (e.g. dashboard state, query builder state).
|
||||
- **Do not use Redux or React Context** for global or feature-level client state.
|
||||
- Prefer small, domain-scoped stores (e.g. DashboardStore, QueryBuilderStore).
|
||||
|
||||
### Zustand best practices (align with eslint-plugin-zustand-rules)
|
||||
|
||||
- **One store per module.** Do not define multiple `create()` calls in the same file; use one store per module (or compose slices into one store).
|
||||
- **Always use selectors.** Call the store hook with a selector so only the used slice triggers re-renders. Never use `useStore()` with no selector.
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD: selector — re-renders only when isDashboardLocked changes
|
||||
const isLocked = useDashboardStore(state => state.isDashboardLocked)
|
||||
|
||||
// ❌ BAD: no selector — re-renders on any store change
|
||||
const state = useDashboardStore()
|
||||
```
|
||||
|
||||
- **Never mutate state directly.** Update only via `set` or `setState` (or `getState()` + `set` for reads). No `state.foo = x` or `state.bears += 1` inside actions.
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD: use set
|
||||
increment: () => set(state => ({ bears: state.bears + 1 }))
|
||||
|
||||
// ❌ BAD: direct mutation
|
||||
increment: () => { state.bears += 1 }
|
||||
```
|
||||
|
||||
- **State properties before actions.** In the store object, list all state fields first, then action functions.
|
||||
- **Split into slices when state is large.** If a store has many top-level properties (e.g. more than 5–10), split into slice factories and combine with one `create()`.
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD: slices for large state
|
||||
const createBearSlice = set => ({ bears: 0, addBear: () => set(s => ({ bears: s.bears + 1 })) })
|
||||
const createFishSlice = set => ({ fish: 0, addFish: () => set(s => ({ fish: s.fish + 1 })) })
|
||||
const useStore = create(set => ({ ...createBearSlice(set), ...createFishSlice(set) }))
|
||||
```
|
||||
|
||||
- **In projects using Zustand:** add `eslint-plugin-zustand-rules` and extend `plugin:zustand-rules/recommended` to enforce these rules automatically.
|
||||
|
||||
## Local state → React state only
|
||||
|
||||
- **Use useState/useReducer** for: component-local UI state, form inputs, toggles, hover state, data that never leaves the component.
|
||||
- Do not use Zustand, Redux, or Context for state that is purely local to one component or a small subtree.
|
||||
|
||||
## Summary
|
||||
|
||||
| State type | Use | Avoid |
|
||||
|-------------------|------------------|--------------------|
|
||||
| Server / API | React Query | Redux, Context |
|
||||
| URL / shareable | nuqs | Redux, Context |
|
||||
| Global client | Zustand | Redux, Context |
|
||||
| Local UI | useState/useReducer | Zustand, Redux, Context |
|
||||
150
frontend/.cursor/skills/migrate-state-management/SKILL.md
Normal file
150
frontend/.cursor/skills/migrate-state-management/SKILL.md
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
name: migrate-state-management
|
||||
description: Migrate Redux or React Context to the correct state option (React Query for server state, nuqs for URL/shareable state, Zustand for global client state). Use when refactoring away from Redux/Context, moving state to the right store, or when the user asks to migrate state management.
|
||||
---
|
||||
|
||||
# Migrate State: Redux/Context → React Query, nuqs, Zustand
|
||||
|
||||
Do **not** introduce or recommend Redux or React Context. Migrate existing usage to the stack below.
|
||||
|
||||
## 1. Classify the state
|
||||
|
||||
Before changing code, classify what the state represents:
|
||||
|
||||
| If the state is… | Migrate to | Do not use |
|
||||
|------------------|------------|------------|
|
||||
| From API / server (versions, configs, fetched lists, time-series) | **React Query** | Redux, Context |
|
||||
| Shareable via URL (filters, time range, page, selected ids) | **nuqs** | Redux, Context |
|
||||
| Global/client UI (dashboard lock, query builder, feature flags, large client objects) | **Zustand** | Redux, Context |
|
||||
| Local to one component (inputs, toggles, hover) | **useState / useReducer** | Zustand, Redux, Context |
|
||||
|
||||
If one slice mixes concerns (e.g. Redux has both API data and pagination), split: API → React Query, pagination → nuqs, rest → Zustand or local state.
|
||||
|
||||
## 2. Migrate to React Query (server state)
|
||||
|
||||
**When:** State comes from or mirrors an API response (e.g. `currentVersion`, `latestVersion`, `configs`, lists).
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Find where the data is fetched (existing `useQuery`/API call) and where it is dispatched or set in Context/Redux.
|
||||
2. Remove the dispatch/set that writes API results into Redux/Context.
|
||||
3. Expose a single hook that uses the query and returns the same shape consumers expect (use `useMemo` for derived objects like `configs` to avoid unnecessary re-renders).
|
||||
4. Replace Redux/Context consumption with the new hook. Prefer generated React Query hooks from `frontend/src/api/generated` when available.
|
||||
5. Configure cache/refetch (e.g. `refetchOnMount: false`, `staleTime`) so behavior matches previous “single source” expectations.
|
||||
|
||||
**Before (Redux mirroring React Query):**
|
||||
|
||||
```tsx
|
||||
if (getUserLatestVersionResponse.isFetched && getUserLatestVersionResponse.isSuccess && getUserLatestVersionResponse.data?.payload) {
|
||||
dispatch({ type: UPDATE_LATEST_VERSION, payload: { latestVersion: getUserLatestVersionResponse.data.payload.tag_name } })
|
||||
}
|
||||
```
|
||||
|
||||
**After (single source in React Query):**
|
||||
|
||||
```tsx
|
||||
export function useAppStateHook() {
|
||||
const { data, isError } = useQuery(...)
|
||||
const memoizedConfigs = useMemo(() => ({ ... }), [data?.configs])
|
||||
return {
|
||||
latestVersion: data?.payload?.tag_name,
|
||||
configs: memoizedConfigs,
|
||||
isError,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Consumers use `useAppStateHook()` instead of `useSelector` or Context. Do not copy React Query result into Redux or Context.
|
||||
|
||||
## 3. Migrate to nuqs (URL / shareable state)
|
||||
|
||||
**When:** State should be in the URL: filters, time range, pagination, selected values, view state. Keep payload small (e.g. Chrome ~2k chars); no large datasets or sensitive data.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Identify which Redux/Context fields are shareable or already reflected in the URL (e.g. `currentPage`, `timeRange`, `selectedFilter`).
|
||||
2. Add nuqs (or use existing): `useQueryState('param', parseAsString.withDefault('…'))` (or `parseAsInteger`, etc.).
|
||||
3. Replace reads/writes of those fields with nuqs hooks. Use typed parsers; avoid ad-hoc `useSearchParams` encoding/decoding.
|
||||
4. Remove the same fields from Redux/Context and their reducers/providers.
|
||||
|
||||
**Before (Context/Redux):**
|
||||
|
||||
```tsx
|
||||
const { timeRange } = useContext(SomeContext)
|
||||
const [page, setPage] = useDispatch(...)
|
||||
```
|
||||
|
||||
**After (nuqs):**
|
||||
|
||||
```tsx
|
||||
const [timeRange, setTimeRange] = useQueryState('timeRange', parseAsString.withDefault('1h'))
|
||||
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
|
||||
```
|
||||
|
||||
## 4. Migrate to Zustand (global client state)
|
||||
|
||||
**When:** State is global or cross-component client state: feature flags, dashboard state, query builder state, complex/large client objects (e.g. up to ~1.5–2MB). Not for server cache or local-only UI.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Create one store per domain (e.g. `DashboardStore`, `QueryBuilderStore`). One `create()` per module; for large state use slice factories and combine.
|
||||
2. Put state properties first, then actions. Use `set` (or `setState` / `getState()` + `set`) for updates; never mutate state directly.
|
||||
3. Replace Context/Redux consumption with the store hook **and a selector** so only the used slice triggers re-renders.
|
||||
4. Remove the old Context provider / Redux slice and related dispatches.
|
||||
|
||||
**Selector (required):**
|
||||
|
||||
```tsx
|
||||
const isLocked = useDashboardStore(state => state.isDashboardLocked)
|
||||
```
|
||||
|
||||
Never use `useStore()` with no selector. Never do `state.foo = x` inside actions; use `set(state => ({ ... }))`.
|
||||
|
||||
**Before (Context/Redux):**
|
||||
|
||||
```tsx
|
||||
const { isDashboardLocked, setLocked } = useContext(DashboardContext)
|
||||
```
|
||||
|
||||
**After (Zustand):**
|
||||
|
||||
```tsx
|
||||
const isLocked = useDashboardStore(state => state.isDashboardLocked)
|
||||
const setLocked = useDashboardStore(state => state.setLocked)
|
||||
```
|
||||
|
||||
For large stores (many top-level fields), split into slices and combine:
|
||||
|
||||
```tsx
|
||||
const createBearSlice = set => ({ bears: 0, addBear: () => set(s => ({ bears: s.bears + 1 })) })
|
||||
const useStore = create(set => ({ ...createBearSlice(set), ...createFishSlice(set) }))
|
||||
```
|
||||
|
||||
Add `eslint-plugin-zustand-rules` with `plugin:zustand-rules/recommended` to enforce selectors and no direct mutation.
|
||||
|
||||
## 5. Migrate to local state (useState / useReducer)
|
||||
|
||||
**When:** State is used only inside one component or a small subtree (form inputs, toggles, hover, panel selection). No URL sync, no cross-feature sharing.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Move the state into the component that owns it (or the smallest common parent).
|
||||
2. Use `useState` or `useReducer` (useReducer when multiple related fields change together).
|
||||
3. Remove from Redux/Context and any provider/slice.
|
||||
|
||||
Do not use Zustand, Redux, or Context for purely local UI state.
|
||||
|
||||
## 6. Migration checklist
|
||||
|
||||
- [ ] Classify each piece of state (server / URL / global client / local).
|
||||
- [ ] Server state: move to React Query; expose via hook; remove Redux/Context mirroring.
|
||||
- [ ] URL state: move to nuqs; remove from Redux/Context; keep URL payload small.
|
||||
- [ ] Global client state: move to Zustand with selectors and immutable updates; one store per domain.
|
||||
- [ ] Local state: move to useState/useReducer in the owning component.
|
||||
- [ ] Remove old Redux slices / Context providers and all dispatches/consumers for migrated state.
|
||||
- [ ] Do not duplicate the same data in multiple places (e.g. React Query + Redux).
|
||||
|
||||
## Additional resources
|
||||
|
||||
- Project rule: [.cursor/rules/state-management.mdc](../../rules/state-management.mdc)
|
||||
- Detailed patterns and rationale: [reference.md](reference.md)
|
||||
@@ -0,0 +1,50 @@
|
||||
# State migration reference
|
||||
|
||||
## Why migrate
|
||||
|
||||
- **Context:** Re-renders all consumers on any change; no granular subscriptions; becomes brittle at scale.
|
||||
- **Redux:** Heavy boilerplate (actions, reducers, selectors, Provider); slower onboarding; often used to mirror React Query or URL state.
|
||||
- **Goal:** Fewer mechanisms, domain isolation, granular subscriptions, single source of truth per state type.
|
||||
|
||||
## React Query migration (server state)
|
||||
|
||||
Typical anti-pattern: API is called via React Query, then result is dispatched to Redux. Flow becomes: Component → useQueries → API → dispatch → Reducer → Redux state → useSelector.
|
||||
|
||||
Correct flow: Component → useQuery (or custom hook wrapping it) → same component reads from hook. No Redux/Context in between.
|
||||
|
||||
- Prefer generated hooks from `frontend/src/api/generated`.
|
||||
- For “app state” that is just API data (versions, configs), one hook that returns `{ ...data, configs: useMemo(...) }` is enough. No selectors needed for plain data; useMemo only where the value is used as dependency (e.g. in useState).
|
||||
- Set `staleTime` / `refetchOnMount` etc. so refetch behavior matches previous expectations.
|
||||
|
||||
## nuqs migration (URL state)
|
||||
|
||||
Redux/Context often hold pagination, filters, time range, selected values that are shareable. Those belong in the URL.
|
||||
|
||||
- Use [nuqs](https://nuqs.dev/docs/basic-usage) for typed search params. Avoid ad-hoc `useSearchParams` + manual encoding.
|
||||
- Browser limits: Chrome ~2k chars practical; keep payload small; no large datasets or secrets in query params.
|
||||
- If the app uses TanStack Router, search params can be handled there; otherwise nuqs is the standard.
|
||||
|
||||
## Zustand migration (client state)
|
||||
|
||||
- One store per domain (e.g. DashboardStore, QueryBuilderStore). Multiple `create()` in one file is disallowed; use one store or composed slices.
|
||||
- Always use a selector: `useStore(s => s.field)` so only that field drives re-renders.
|
||||
- Never mutate: update only via `set(state => ({ ... }))` or `setState` / `getState()` + `set`.
|
||||
- State properties first, then actions. For 5–10+ top-level fields, split into slice factories and combine with one `create()`.
|
||||
- Large client objects: Zustand is for “large” in the ~1.5–2MB range; above that, optimize at API/store design.
|
||||
- Testing: no Provider; stores are plain functions; easy to reset and mock.
|
||||
|
||||
## What not to use
|
||||
|
||||
- **Redux / Context** for new or migrated shared/global state.
|
||||
- **Redux / Context** to store or mirror React Query results.
|
||||
- **Redux / Context** for state that should live in the URL (use nuqs).
|
||||
- **Zustand / Redux / Context** for component-local UI (use useState/useReducer).
|
||||
|
||||
## Summary table
|
||||
|
||||
| State type | Use | Avoid |
|
||||
|-------------|--------------------|-----------------|
|
||||
| Server/API | React Query | Redux, Context |
|
||||
| URL/shareable | nuqs | Redux, Context |
|
||||
| Global client | Zustand | Redux, Context |
|
||||
| Local UI | useState/useReducer | Zustand, Redux, Context |
|
||||
Reference in New Issue
Block a user