UNPKG

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
--- 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.