agent-rules-kit
Version:
CLI tool to bootstrap AI agent rules for multiple IDEs and frameworks. Generates optimized rules for Cursor, VS Code, Claude, and 6+ other AI coding assistants.
498 lines (415 loc) • 13 kB
Markdown
---
description: Guide for using Redux state management in React applications
globs: <root>/src/**/*.ts,<root>/src/**/*.tsx,<root>/src/**/*.js,<root>/src/**/*.jsx
alwaysApply: false
---
# Redux State Management in React
Redux is a predictable state container for JavaScript applications, commonly used with React. It helps you manage global application state in a predictable and maintainable way.
## Core Concepts
1. **Store**: A single source of truth that holds the entire application state
2. **Actions**: Plain JavaScript objects that describe what happened
3. **Reducers**: Pure functions that specify how the state changes in response to actions
4. **Selectors**: Functions to extract specific pieces of state
5. **Middleware**: Extends Redux with custom functionality (async operations, logging, etc.)
## Directory Structure
```
src/
├── store/
│ ├── index.js # Store configuration
│ ├── rootReducer.js # Root reducer combining all feature reducers
│ ├── middleware.js # Custom middleware configuration
│ └── hooks.js # Custom Redux hooks
├── features/
│ ├── auth/
│ │ ├── authSlice.js # Redux Toolkit slice (actions + reducer)
│ │ ├── authSelectors.js # Selectors for auth state
│ │ └── authThunks.js # Async thunks for auth operations
│ └── products/
│ ├── productsSlice.js # Redux Toolkit slice for products
│ ├── productsSelectors.js
│ └── productsThunks.js
└── services/
└── api.js # API client for data fetching
```
## Implementation with Redux Toolkit
### Store Setup
```javascript
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import rootReducer from './rootReducer';
import middleware from './middleware';
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(middleware),
devTools: process.env.NODE_ENV !== 'production',
});
// Optional: Setup listeners for RTK Query
setupListeners(store.dispatch);
export default store;
```
```javascript
// store/rootReducer.js
import { combineReducers } from '@reduxjs/toolkit';
import authReducer from '../features/auth/authSlice';
import productsReducer from '../features/products/productsSlice';
import { api } from '../services/api';
const rootReducer = combineReducers({
auth: authReducer,
products: productsReducer,
[api.reducerPath]: api.reducer,
});
export default rootReducer;
```
```javascript
// store/middleware.js
import { api } from '../services/api';
const middleware = [
api.middleware,
// Add additional middleware here
];
export default middleware;
```
```javascript
// store/hooks.js
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './types';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```
```typescript
// store/types.ts
import { store } from './index';
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
```
### Feature Slice Example
```javascript
// features/auth/authSlice.js
import { createSlice } from '@reduxjs/toolkit';
import { loginUser, logoutUser, registerUser } from './authThunks';
const initialState = {
user: null,
token: localStorage.getItem('token') || null,
isAuthenticated: !!localStorage.getItem('token'),
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
// Login
.addCase(loginUser.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.status = 'succeeded';
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
localStorage.setItem('token', action.payload.token);
})
.addCase(loginUser.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || 'Login failed';
})
// Logout
.addCase(logoutUser.fulfilled, (state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
localStorage.removeItem('token');
})
// Register
.addCase(registerUser.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(registerUser.fulfilled, (state, action) => {
state.status = 'succeeded';
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
localStorage.setItem('token', action.payload.token);
})
.addCase(registerUser.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || 'Registration failed';
});
},
});
export const { clearError } = authSlice.actions;
export default authSlice.reducer;
```
### Thunks for Async Operations
```javascript
// features/auth/authThunks.js
import { createAsyncThunk } from '@reduxjs/toolkit';
import api from '../../services/api';
export const loginUser = createAsyncThunk(
'auth/login',
async (credentials, { rejectWithValue }) => {
try {
const response = await api.post('/auth/login', credentials);
return response.data;
} catch (error) {
return rejectWithValue(
error.response?.data?.message || 'Login failed'
);
}
}
);
export const logoutUser = createAsyncThunk(
'auth/logout',
async (_, { rejectWithValue }) => {
try {
await api.post('/auth/logout');
return {};
} catch (error) {
return rejectWithValue(
error.response?.data?.message || 'Logout failed'
);
}
}
);
export const registerUser = createAsyncThunk(
'auth/register',
async (userData, { rejectWithValue }) => {
try {
const response = await api.post('/auth/register', userData);
return response.data;
} catch (error) {
return rejectWithValue(
error.response?.data?.message || 'Registration failed'
);
}
}
);
```
### Selectors
```javascript
// features/auth/authSelectors.js
export const selectUser = (state) => state.auth.user;
export const selectIsAuthenticated = (state) => state.auth.isAuthenticated;
export const selectAuthStatus = (state) => state.auth.status;
export const selectAuthError = (state) => state.auth.error;
```
### Usage in Components
```jsx
// components/LoginForm.jsx
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { loginUser } from '../../features/auth/authThunks';
import { clearError } from '../../features/auth/authSlice';
import {
selectAuthStatus,
selectAuthError,
selectIsAuthenticated,
} from '../../features/auth/authSelectors';
const LoginForm = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const status = useAppSelector(selectAuthStatus);
const error = useAppSelector(selectAuthError);
const isAuthenticated = useAppSelector(selectIsAuthenticated);
// Redirect if already authenticated
useEffect(() => {
if (isAuthenticated) {
navigate('/dashboard');
}
}, [isAuthenticated, navigate]);
// Clear errors when unmounting
useEffect(() => {
return () => {
dispatch(clearError());
};
}, [dispatch]);
const handleSubmit = (e) => {
e.preventDefault();
dispatch(loginUser({ email, password }));
};
return (
<form onSubmit={handleSubmit}>
{error && <div className='error-message'>{error}</div>}
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
id='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
id='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type='submit' disabled={status === 'loading'}>
{status === 'loading' ? 'Logging in...' : 'Login'}
</button>
</form>
);
};
export default LoginForm;
```
## Using RTK Query for API Calls
Redux Toolkit Query (RTK Query) is a powerful data fetching and caching tool included in Redux Toolkit.
```javascript
// services/api.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
// Get token from auth state
const token = getState().auth.token;
// If token exists, add to headers
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
},
}),
endpoints: (builder) => ({
getProducts: builder.query({
query: () => 'products',
providesTags: ['Products'],
}),
getProduct: builder.query({
query: (id) => `products/${id}`,
providesTags: (result, error, id) => [{ type: 'Products', id }],
}),
addProduct: builder.mutation({
query: (product) => ({
url: 'products',
method: 'POST',
body: product,
}),
invalidatesTags: ['Products'],
}),
updateProduct: builder.mutation({
query: ({ id, ...patch }) => ({
url: `products/${id}`,
method: 'PATCH',
body: patch,
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'Products', id },
],
}),
deleteProduct: builder.mutation({
query: (id) => ({
url: `products/${id}`,
method: 'DELETE',
}),
invalidatesTags: ['Products'],
}),
}),
});
export const {
useGetProductsQuery,
useGetProductQuery,
useAddProductMutation,
useUpdateProductMutation,
useDeleteProductMutation,
} = api;
```
### Using RTK Query Hooks
```jsx
// components/ProductsList.jsx
import React from 'react';
import { useGetProductsQuery } from '../../services/api';
const ProductsList = () => {
const { data: products, isLoading, error } = useGetProductsQuery();
if (isLoading) return <div>Loading products...</div>;
if (error) return <div>Error loading products: {error.message}</div>;
return (
<div className='products-list'>
<h2>Products</h2>
<ul>
{products.map((product) => (
<li key={product.id}>
<h3>{product.name}</h3>
<p>${product.price}</p>
</li>
))}
</ul>
</div>
);
};
export default ProductsList;
```
## Application Setup
```jsx
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import store from './store';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>
);
```
## Best Practices
1. **Use Redux Toolkit**
- Simplifies store setup and reduces boilerplate
- Includes utilities for common patterns
- Enforces Redux best practices
2. **Follow the Ducks Pattern**
- Group related actions, reducers, and selectors by feature
- Use Redux Toolkit's `createSlice` to define reducers and actions together
3. **Keep Reducers Pure**
- Reducers should be pure functions without side effects
- Use middleware like Thunks for side effects (API calls, etc.)
4. **Normalize Complex State**
- Use a normalized state structure for collections of items
- Consider using `createEntityAdapter` for CRUD operations
5. **Use Selectors for Data Access**
- Create selector functions to access state
- Use `reselect` for memoized selectors to optimize performance
6. **Typescript Integration**
- Define types for state and actions
- Use typed hooks (`useAppDispatch`, `useAppSelector`)
7. **Testing**
- Test reducers as pure functions
- Test selectors with known state
- Use mock store for testing connected components
## When to Use Redux
Redux is most beneficial in the following scenarios:
- Complex applications with significant state management needs
- Large applications where state is shared across many components
- Applications with complex state transitions and side effects
- Apps that need time-travel debugging
- When you need a single source of truth for application state
For smaller applications or when you only need local state, consider using React's built-in state management (useState, useReducer, Context API) instead.