"""markdown
# Core Architecture Standards for Redux
This document outlines the core architectural standards for building Redux applications. Adhering to these standards will promote maintainability, scalability, testability, and performance. These standards are based on the latest best practices and recommendations from the Redux documentation and community.
## 1. Fundamental Architectural Patterns
### 1.1 Ducks Pattern
**Description:** The Ducks pattern proposes that all Redux logic for a module (i.e., reducer, actions, action types, and selectors) reside in a single file.
**Why:** Enhances modularity, organization, and discoverability.
**Do This:**
* Structure your Redux logic using the Ducks pattern. Each module (feature) should have its own file containing the reducer, action creators, action types, and selectors.
**Don't Do This:**
* Scatter Redux logic across multiple disconnected files.
**Example:**
"""javascript
// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0,
status: 'idle',
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
});
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// Async thunk action
export const incrementAsync = (amount) => async (dispatch) => {
// Simulate an API call
await new Promise((resolve) => setTimeout(resolve, 500));
dispatch(incrementByAmount(amount));
};
export const selectCount = (state) => state.counter.value;
export default counterSlice.reducer;
"""
### 1.2 Feature Slices (Redux Toolkit)
**Description**: Redux Toolkit's "createSlice" simplifies reducer and action creation.
**Why**: Reduces boilerplate, promotes immutability, and offers a structured approach.
**Do This**:
* Use "createSlice" from Redux Toolkit to define reducers and actions in a single place and automatically generate action creators and action types.
* Leverage the "immer" library, which is integrated into "createSlice", to write simpler immutable update logic directly.
**Don't Do This**:
* Manually create reducers, action types, and action creators using switch statements or other verbose techniques.
**Example**:
"""javascript
// src/features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Define the async thunk
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5');
const data = await response.json();
return data;
}
);
const initialState = {
todos: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null
}
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
state.todos.push(action.payload)
},
todoToggled(state, action) {
const todoId = action.payload
const todo = state.todos.find(todo => todo.id === todoId)
if (todo) {
todo.completed = !todo.completed
}
},
todoDeleted(state, action) {
const todoId = action.payload;
state.todos = state.todos.filter(todo => todo.id !== todoId);
}
},
extraReducers(builder) {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded'
// Add any fetched posts to the array
state.todos = state.todos.concat(action.payload)
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
})
export const { todoAdded, todoToggled, todoDeleted } = todosSlice.actions
export const selectAllTodos = (state) => state.todos.todos
export const selectTodosStatus = (state) => state.todos.status
export const selectTodosError = (state) => state.todos.error
export default todosSlice.reducer
"""
### 1.3 Async Actions with Thunks
**Description**: Thunks are a standard way to handle asynchronous logic in Redux.
**Why**: Enables side effects (API calls, etc.) within your Redux workflow.
**Do This**:
* Use Redux Thunk middleware (or another middleware like Redux Saga or Redux Observable).
* Dispatch thunk action creators to perform asynchronous tasks and dispatch appropriate actions based on the outcome (e.g., success, failure).
* Use "createAsyncThunk" from Redux Toolkit simplify thunk creation.
**Don't Do This**:
* Perform asynchronous operations directly within reducers. Reducers must be pure functions.
* Mutate state directly within asynchronous actions.
**Example**:
"""javascript
// src/features/users/usersSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Define an async thunk action
export const fetchUsers = createAsyncThunk(
'users/fetchUsers', // Action type prefix
async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
return data;
}
);
const initialState = {
users: [],
status: 'idle',
error: null
};
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// Define synchronous reducer actions here, if any
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.users = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
}
});
export const selectAllUsers = (state) => state.users.users;
export const selectUsersStatus = (state) => state.users.status;
export const selectUsersError = (state) => state.users.error;
export default usersSlice.reducer;
// Usage in component:
// import { useDispatch, useSelector } from 'react-redux';
// import { useEffect } from 'react';
// import { fetchUsers, selectAllUsers, selectUsersStatus, selectUsersError } from './usersSlice';
// function UsersList() {
// const dispatch = useDispatch();
// const users = useSelector(selectAllUsers);
// const status = useSelector(selectUsersStatus);
// const error = useSelector(selectUsersError);
// useEffect(() => {
// if (status === 'idle') {
// dispatch(fetchUsers());
// }
// }, [status, dispatch]);
// if (status === 'loading') {
// return Loading users...;
// }
// if (status === 'failed') {
// return Error: {error};
// }
// return (
//
// {users.map(user => (
// {user.name}
// ))}
//
// );
// }
// export default UsersList;
"""
### 1.4 Selectors
**Description**: Selectors are functions used to extract specific pieces of data from the Redux store.
**Why**: Encapsulate store structure, improve performance by memoizing selections, and simplify component access to data.
**Do This:**
* Create selectors for accessing data from the store, especially for complex data transformations or derivations.
* Use memoizing selectors (e.g., with "createSelector" from Reselect) to prevent unnecessary re-renders.
**Don't Do This:**
* Directly access state in components without selectors, tightly coupling components to the store structure.
* Perform complex data transformations directly in components.
**Example:**
"""javascript
// src/features/posts/postsSlice.js
import { createSlice, createSelector } from '@reduxjs/toolkit';
import { nanoid } from '@reduxjs/toolkit';
const initialState = {
posts: [
{ id: '1', title: 'First Post!', content: 'Hello!', reactions: {thumbsUp: 0, wow: 0, heart: 0, rocket: 0, coffee: 0} },
{ id: '2', title: 'Second Post', content: 'More text', reactions: {thumbsUp: 0, wow: 0, heart: 0, rocket: 0, coffee: 0} }
]
};
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded: {
reducer(state, action) {
state.posts.push(action.payload)
},
prepare(title, content, userId) {
return {
payload: {
id: nanoid(),
title,
content,
user: userId,
date: new Date().toISOString(),
reactions: {
thumbsUp: 0,
wow: 0,
heart: 0,
rocket: 0,
coffee: 0
}
}
}
}
},
reactionAdded(state, action) {
const { postId, reaction } = action.payload
const existingPost = state.posts.find(post => post.id === postId)
if (existingPost) {
existingPost.reactions[reaction]++
}
}
}
})
export const { postAdded, reactionAdded } = postsSlice.actions
export default postsSlice.reducer
// Selectors
export const selectAllPosts = (state) => state.posts.posts
//Memoized selector using createSelector
export const selectPostById = (state, postId) => state.posts.posts.find(post => post.id === postId)
//Example Memoized Selector using createSelector
import { createSelector } from '@reduxjs/toolkit'
const selectPosts = state => state.posts.posts
export const selectPostsByUser = createSelector(
[selectPosts, (state, userId) => userId],
(posts, userId) => posts.filter(post => post.user === userId)
)
//Usage in component:
// const post = useSelector((state) => selectPostById(state, postId))
"""
## 2. Project Structure and Organization
### 2.1 Modular Directory Structure
**Description**: Organize your Redux code into logically grouped modules or "feature folders."
**Why**: Enhances maintainability, promotes code reuse, and reduces complexity.
**Do This**:
* Structure your project using a modular directory structure, such as:
"""
src/
├── app/ # Core application setup (store, middleware, root reducer)
│ ├── store.js # Redux store configuration using Redux Toolkit.
│ └── rootReducer.js # Root reducer combining all feature reducers
├── features/ # Feature-specific code
│ ├── counter/ # Example feature: counter
│ │ ├── counterSlice.js # Redux slice (reducer, actions, selectors)
│ │ ├── Counter.js # React component for the counter feature
│ │ └── Counter.module.css # Styles specific to the Counter component
│ ├── todos/ # Example feature: todos
│ │ ├── todosSlice.js # Redux slice for todos
│ │ ├── TodoList.js # React component to display a list of todos
│ │ └── ...
│ └── ...
├── components/ # Reusable UI components
│ ├── Button.js
│ ├── Input.js
│ └── ...
├── api/ # API client code
│ ├── userApi.js
│ ├── postApi.js
│ └── ...
├── utils/ # Utility functions
├── hooks/ # Custom React hooks
└── index.js # Top-level application entry point
"""
**Don't Do This**:
* Dump all your Redux code into a single "reducers" or "actions" folder.
### 2.2 Centralized Store Configuration
**Description**: Define and configure your Redux store in a dedicated module.
**Why**: Provides a single source of truth for store-related settings and middleware.
**Do This**:
* Use [Redux Toolkit's "configureStore"](https://redux-toolkit.js.org/api/configureStore) function to set up your Redux store. This simplifies the setup process and handles common configurations like middleware (Thunk, DevTools) automatically.
**Don't Do This**:
* Set up the Redux store directly using the legacy "createStore" function without incorporating Redux Toolkit's utilities.
**Example**:
"""javascript
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todosReducer from '../features/todos/todosSlice';
import usersReducer from '../features/users/usersSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todosReducer,
users: usersReducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware(), // Example to customize middleware
devTools: process.env.NODE_ENV !== 'production', // Enable Redux DevTools in development
});
"""
### 2.3 Group Similar Files
**Description**: Group files related to single functionality together.
**Why**: Improves readability, organization and maintainability.
**Do This**:
* Group related files like components, styles, and tests together. For example:
"""
src/
├── components/
│ ├── Button/
│ │ ├── Button.js
│ │ ├── Button.module.css
│ │ └── Button.test.js
"""
**Don't Do This**:
* Keep components, styles, and test files in seperate unrelated folders.
## 3. Best Practices for Redux Development
### 3.1 Immutability
**Description**: Reducers must update state immutably, returning new state objects instead of modifying existing ones.
**Why**: Enables change detection, simplifies debugging, and enhances performance, especially with React's reconciliation process.
**Do This**:
* Use immutable update patterns:
* For objects: "{ ...state, newValue: 'something' }"
* For arrays: "state.slice().concat(newValue)" or "[...state, newValue]"
* Use Immer library, which is built into Redux Toolkit's "createSlice", to write immutable updates with mutable-like syntax.
**Don't Do This**:
* Mutate state directly: "state.value = newValue".
**Example (Immer)**:
"""javascript
// Example slice using Immer.
const initialState = {
data: []
}
const dataSlice = createSlice({
name: 'name',
initialState : initialState,
reducers : {
addItem: (state, action) => {
state.data.push(action.payload); // Immer allows you to write mutations like this
}
}
});
"""
### 3.2 Normalizing State
**Description**: Store relational data in a normalized format, using IDs as keys.
**Why**: Reduces data duplication, simplifies updates, and improves performance.
**Do This**:
* Structure your state to avoid nested objects.
* Store entities in separate dictionaries, keyed by their IDs.
* Use selectors to derive denormalized data for components.
**Don't Do This**:
* Store denormalized or duplicated data.
**Example**:
"""javascript
// Normalized state structure
const initialState = {
posts: {
byId: {
'post1': { id: 'post1', title: 'First Post', author: 'user1' },
'post2': { id: 'post2', title: 'Second Post', author: 'user2' },
},
allIds: ['post1', 'post2'],
},
users: {
byId: {
'user1': { id: 'user1', name: 'Alice' },
'user2': { id: 'user2', name: 'Bob' },
},
},
};
// Selector to get post with author
const selectPostWithAuthor = (state, postId) => {
const post = state.posts.byId[postId];
const author = state.users.byId[post.author];
return { ...post, author };
};
"""
### 3.3 Avoid Storing Non-Serializable Data in Store
**Description**: Only store serializable data in your Redux store.
**Why**: Serializability is important for debugging (time travel), persistence (saving/restoring state), and server-side rendering.
**Do This**:
* Store primitive values (strings, numbers, booleans), plain JavaScript objects, and arrays.
**Don't Do This**:
* Store functions, Promises, class instances, or Date objects directly in your Redux store. If you need to store such data, consider transforming it into a serializable representation (e.g., converting a "Date" object to an ISO string).
### 3.4 Optimize Performance
**Description**: Techniques and methods used to improve runtime.
**Why**: Avoid performance bottleneck and ensure a smooth applications
**Do This**:
* Only connect components that need to access Redux state using "connect" or "useSelector".
* Use "React.memo" to prevent unnecessary re-renders of components that receive props from Redux.
* Batch dispatches when updating multiple slices of state to prevent multiple renders.
* Use Reselect or similar libraries to memoize selectors, so they only recompute when their inputs change.
**Don't Do This**:
* Connect every component to the store without considering whether it needs live updates.
* Over-optimize selectors or components, which can add unnecessary complexity.
### 3.5 Testing Redux Logic
**Description**: Write automated tests.
**Why**: Verify correctness and prevent regressions..
**Do This**:
* Test reducers to ensure they produce the correct state for different actions.
* Test action creators to ensure they return the expected actions.
* If using thunks, mock API calls and assert that the correct actions are dispatched.
* Utilize tools like Jest and Redux Mock Store for comprehensive testing.
**Example**:
"""javascript
// counterSlice.test.js
import counterReducer, { increment, decrement, incrementByAmount } from './counterSlice';
describe('counter reducer', () => {
const initialState = {
value: 3,
status: 'idle',
};
it('should handle initial state', () => {
expect(counterReducer(undefined, { type: 'unknown' })).toEqual({
value: 0,
status: 'idle',
});
});
it('should handle increment', () => {
const actual = counterReducer(initialState, increment());
expect(actual.value).toEqual(4);
});
it('should handle decrement', () => {
const actual = counterReducer(initialState, decrement());
expect(actual.value).toEqual(2);
});
it('should handle incrementByAmount', () => {
const actual = counterReducer(initialState, incrementByAmount(2));
expect(actual.value).toEqual(5);
});
});
"""
By adhering to these coding standards, your Redux applications will be well-structured, maintainable, and testable, leading to a more enjoyable and efficient development experience.
"""
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Redux This document outlines the coding standards for designing React components within a Redux application. It focuses on creating reusable, maintainable, and performant components that interact effectively with the Redux store. These guidelines align with the latest Redux practices and ecosystem tools. ## 1. Component Types and Responsibilities ### 1.1 Functional vs. Class Components **Standard:** Favor functional components with hooks for most UI elements. Use class components only when necessary (e.g., for lifecycle methods not easily replicated with hooks, though these cases are increasingly rare). **Do This:** """jsx // Functional component with Redux hooks import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement } from './counterSlice'; function Counter() { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return ( <div> <button onClick={() => dispatch(decrement())}>-</button> <span>{count}</span> <button onClick={() => dispatch(increment())}>+</button> </div> ); } export default Counter; """ **Don't Do This:** """jsx // Avoid unless absolutely necessary import React from 'react'; import { connect } from 'react-redux'; import { increment, decrement } from './actions'; class Counter extends React.Component { render() { return ( <div> <button onClick={this.props.decrement}>-</button> <span>{this.props.count}</span> <button onClick={this.props.increment}>+</button> </div> ); } } const mapStateToProps = (state) => ({ count: state.count, }); const mapDispatchToProps = { increment, decrement }; export default connect(mapStateToProps, mapDispatchToProps)(Counter); """ **Why:** Functional components with hooks: * Are more concise and easier to read. * Promote better code reuse through custom hooks. * Tend to perform better due to simpler lifecycle. * Align with modern React development practices. ### 1.2 Presentational vs. Container Components (Smart vs. Dumb Components) **Standard:** Separate components into presentational (dumb) and container (smart) components. Container components handle Redux logic and pass data to presentational components. **Do This:** """jsx // Presentational Component (DisplayCounter.jsx) import React from 'react'; function DisplayCounter({ count, onIncrement, onDecrement }) { return ( <div> <button onClick={onDecrement}>-</button> <span>{count}</span> <button onClick={onIncrement}>+</button> </div> ); } export default DisplayCounter; // Container Component (CounterContainer.jsx) import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement } from './counterSlice'; import DisplayCounter from './DisplayCounter'; function CounterContainer() { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return ( <DisplayCounter count={count} onIncrement={() => dispatch(increment())} onDecrement={() => dispatch(decrement())} /> ); } export default CounterContainer; """ **Don't Do This:** """jsx // Mixing Redux logic and presentation logic in one component. (Anti-pattern) import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement } from './counterSlice'; function Counter() { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return ( <div> <button onClick={() => dispatch(decrement())}>-</button> <span>{count}</span> <button onClick={() => dispatch(increment())}>+</button> </div> ); } export default Counter; """ **Why:** * **Separation of Concerns:** Makes components easier to understand, test, and reuse. * **Testability:** Presentational components can be easily tested with different props without Redux dependencies. * **Reusability:** Presentational components can be reused in different parts of the application with different data sources. ### 1.3 Component Composition **Standard:** Favor composition over inheritance. Use React's "children" prop to create flexible and reusable components. Consider using render props or function as children patterns for more advanced use cases. **Do This:** """jsx // Layout Component import React from 'react'; function Layout({ children }) { return ( <div className="layout"> <header>Header</header> <main>{children}</main> <footer>Footer</footer> </div> ); } export default Layout; // Usage in another component import React from 'react'; import Layout from './Layout'; function HomePage() { return ( <Layout> <h1>Welcome to the Home Page</h1> <p>Some content here.</p> </Layout> ); } export default HomePage; """ **Don't Do This:** * Creating deeply nested component hierarchies with complex inheritance. * Overusing higher-order components (HOCs) when composition can achieve the same result. While HOCs can be useful, excessive use can lead to "wrapper hell" and difficulty in debugging. Consider hooks as a more modern alternative. **Why:** * **Flexibility:** Composition allows you to combine components in various ways to create different UI structures. * **Readability:** Easier to understand the relationship between components. * **Maintainability:** Changes in one component are less likely to affect other components. ## 2. Data Fetching and Side Effects ### 2.1 Redux Thunk/Saga for Asynchronous Actions **Standard:** Use Redux Thunk or Redux Saga for handling asynchronous actions like API calls. "createAsyncThunk" from Redux Toolkit is the most commonly used and recommended approach. **Do This (using "createAsyncThunk"):** """javascript // counterSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; // Async thunk to fetch data export const fetchData = createAsyncThunk( 'counter/fetchData', async (amount) => { const response = await fetch('https://api.example.com/data'); const data = await response.json(); return data; } ); export const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, data: null, loading: false, error: null }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, }, extraReducers: (builder) => { builder .addCase(fetchData.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchData.fulfilled, (state, action) => { state.loading = false; state.data = action.payload; }) .addCase(fetchData.rejected, (state, action) => { state.loading = false; state.error = action.error.message; }); }, }); export const { increment, decrement } = counterSlice.actions; export default counterSlice.reducer; // Component Usage import React, { useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { fetchData } from './counterSlice'; function MyComponent() { const dispatch = useDispatch(); const data = useSelector((state) => state.counter.data); const loading = useSelector((state) => state.counter.loading); const error = useSelector((state) => state.counter.error); useEffect(() => { dispatch(fetchData()); }, [dispatch]); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error}</p>; return ( <div> {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>No data</p>} </div> ); } export default MyComponent; """ **Don't Do This:** """jsx // Avoid performing side effects directly in the component. (Anti-pattern for Redux) import React, { useEffect, useState } from 'react'; function MyComponent() { const [data, setData] = useState(null); useEffect(() => { fetch('https://api.example.com/data') .then((response) => response.json()) .then((data) => setData(data)); }, []); return ( <div> {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Loading...</p>} </div> ); } export default MyComponent; """ **Why:** * **Centralized State Management:** Redux Thunk/Saga allows you to manage asynchronous operations and update the Redux store in a predictable way. * **Testability:** Easier to test asynchronous logic by mocking the API calls and dispatching actions. * **Maintainability:** Decouples side effects from the component logic, making the code easier to understand and maintain. "createAsyncThunk" further improves this by handling the pending/fulfilled/rejected states automatically. ### 2.2 Selectors for Data Retrieval **Standard:** Use selectors to retrieve data from the Redux store. Create memoized selectors with "createSelector" from Reselect or "@reduxjs/toolkit". **Do This:** """javascript // selectors.js (using Reselect) import { createSelector } from 'reselect'; const selectCounter = (state) => state.counter; export const selectCount = createSelector( [selectCounter], (counter) => counter.value ); export const selectData = createSelector( [selectCounter], (counter) => counter.data ); //Component import React from 'react'; import { useSelector } from 'react-redux'; import { selectCount, selectData } from './selectors'; function MyComponent() { const count = useSelector(selectCount); const data = useSelector(selectData); return ( <div> <p>Count: {count}</p> {data && <pre>{JSON.stringify(data, null, 2)}</pre>} </div> ); } export default MyComponent; """ **Don't Do This:** """jsx // Avoid accessing state directly in the component. (This is less efficient because React re-renders components when the Redux store changes.) import React from 'react'; import { useSelector } from 'react-redux'; function MyComponent() { const count = useSelector((state) => state.counter.value); const data = useSelector((state) => state.counter.data); return ( <div> <p>Count: {count}</p> {data && <pre>{JSON.stringify(data, null, 2)}</pre>} </div> ); } export default MyComponent; """ **Why:** * **Performance:** Memoized selectors prevent unnecessary re-renders by returning the cached result if the input selectors haven't changed. "useSelector" hook does shallow comparison of previous and next state, which can cause unecessary rerenders. * **Abstraction:** Selectors abstract the state structure, making it easier to refactor the Redux store without affecting the components. * **Testability:** Selectors can be easily tested in isolation. ## 3. Component Performance Optimization ### 3.1 Memoization **Standard:** Use "React.memo" to memoize functional components. Use "useMemo" and "useCallback" hooks inside functional components to memoize values and functions. "React.memo" is particularly effective for presentational components that receive props from container components. **Do This:** """jsx // Memoizing a functional component import React from 'react'; const DisplayCounter = React.memo(function DisplayCounter({ count, onIncrement, onDecrement }) { console.log("DisplayCounter rendered"); // Check when it re-renders return ( <div> <button onClick={onDecrement}>-</button> <span>{count}</span> <button onClick={onIncrement}>+</button> </div> ); }); export default DisplayCounter; // Memoizing values and functions inside a functional component import React, { useState, useMemo, useCallback } from 'react'; function MyComponent({ items }) { const [filter, setFilter] = useState(''); // Memoize a derived value const filteredItems = useMemo(() => { console.log("Filtering items"); //check when it filters return items.filter(item => item.name.includes(filter)); }, [items, filter]); // Memoize a callback function const handleFilterChange = useCallback((event) => { setFilter(event.target.value); }, []); return ( <div> <input type="text" value={filter} onChange={handleFilterChange} /> <ul> {filteredItems.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> </div> ); } export default React.memo(MyComponent); """ **Don't Do This:** * Avoid over-memoizing components, as the memoization process itself has a cost. Memoize only components that are likely to re-render unnecessarily. * Don't forget the dependency array in "useMemo" and "useCallback". Missing dependencies can lead to stale values and incorrect behavior. **Why:** * **Performance:** Prevents unnecessary re-renders of components and recalculations of values, improving the overall performance of the application. * **Optimization:** Focuses optimization efforts on the most critical parts of the application. * **Efficiency:** Reduces the amount of work React needs to do, resulting in faster UI updates. ### 3.2 Immutability **Standard:** Ensure that all data transformations in reducers and components are immutable. Use immutable data structures. Redux Toolkit's "createSlice" and Immer simplify immutable updates. **Do This (using Redux Toolkit):** """javascript // counterSlice.js import { createSlice } from '@reduxjs/toolkit'; export const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, items: [{ id: 1, name: 'Item 1' }] }, reducers: { increment: (state) => { state.value += 1; // Immer makes this immutable }, addItem: (state, action) => { state.items.push(action.payload); //Immer makes this immutable }, removeItem: (state, action) => { state.items = state.items.filter((item) => item.id !== action.payload); // Correct immutable update }, }, }); export const { increment, addItem, removeItem } = counterSlice.actions; export default counterSlice.reducer; """ **Don't Do This:** """javascript // Avoid direct mutation of the state. (Anti-pattern) import { createSlice } from '@reduxjs/toolkit'; export const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, items: [{ id: 1, name: 'Item 1' }] }, reducers: { increment: (state) => { state.value++; // Direct mutation }, addItem: (state, action) => { state.items.push(action.payload); // Direct mutation }, removeItem: (state, action) => { delete state.items[action.payload]; // MUTATES THE ARRAY }, }, }); export const { increment, addItem, removeItem } = counterSlice.actions; export default counterSlice.reducer; """ **Why:** * **Predictability:** Immutable data structures make it easier to reason about state changes and debug issues. * **Performance:** React can optimize re-renders by comparing references of immutable objects. * **Time Travel Debugging:** Redux DevTools rely on immutability to implement time travel debugging. ## 4. Testing ### 4.1 Unit Testing **Standard:** Write unit tests for all components, reducers, actions, and selectors. Use testing libraries like Jest and React Testing Library. **Do This:** """jsx // Counter.test.js import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; import Counter from './Counter'; import { increment, decrement } from './counterSlice'; const mockStore = configureStore([]); describe('Counter Component', () => { let store; beforeEach(() => { store = mockStore({ counter: { value: 0 }, // Initial state for the counter slice }); store.dispatch = jest.fn(); // Mock dispatch to observe actions }); it('should render the initial count', () => { render( <Provider store={store}> <Counter /> </Provider> ); expect(screen.getByText('0')).toBeInTheDocument(); }); it('should dispatch increment action when + button is clicked', () => { render( <Provider store={store}> <Counter /> </Provider> ); fireEvent.click(screen.getByText('+')); expect(store.dispatch).toHaveBeenCalledWith(increment()); }); it('should dispatch decrement action when - button is clicked', () => { render( <Provider store={store}> <Counter /> </Provider> ); fireEvent.click(screen.getByText('-')); expect(store.dispatch).toHaveBeenCalledWith(decrement()); }); }); """ **Why:** * **Regression Prevention:** Unit tests help prevent regressions by ensuring that existing functionality continues to work as expected. * **Code Quality:** Writing tests encourages you to write cleaner, more modular code. * **Confidence:** Provides confidence when refactoring or making changes to the codebase. ### 4.2 Integration Testing **Standard:** Write integration tests to verify that components interact correctly with the Redux store and other parts of the application. **Do This:** * Use a testing framework like Cypress or Jest with React Testing Library to simulate user interactions and verify state changes. ### 4.3 End-to-End (E2E) Testing **Standard:** Implement E2E tests using tools like Cypress or Puppeteer to test the entire application flow from the user's perspective. ## 5. Naming Conventions and File Structure ### 5.1 Component Naming **Standard:** Use PascalCase for component names (e.g., "MyComponent", "UserProfile"). ### 5.2 Redux Related Files **Standard:** Use camelCase for action types and action creators (e.g., "incrementCounter", "FETCH_DATA_SUCCESS"). Use slices with Redux Toolkit for reducers and actions. ### 5.3 File Structure **Standard:** Organize components into directories based on features or modules. Keep all React and Redux-related files for a specific module in a directory. """ src/ components/ Counter/ Counter.jsx Counter.test.js counterSlice.js (Redux Toolkit slice with reducer and actions) selectors.js UserProfile/ UserProfile.jsx UserProfile.test.js userSlice.js selectors.js app/ store.js (Redux store configuration) """ ## 6. Error Handling ### 6.1 Centralized Error Handling **Standard:** Implement a centralized error handling mechanism to catch and log errors that occur in components and Redux actions. **Do This:** * Use a try-catch block in asynchronous actions (Redux Thunk/Saga) to catch errors and dispatch an error action to the Redux store. * Create an error reducer to store the error state and display error messages to the user. ### 6.2 Error Boundaries **Standard:** Use React Error Boundaries to catch errors that occur during rendering and prevent the entire application from crashing. **Do This:** """jsx // ErrorBoundary.js import React, { Component } from 'react'; class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service console.error(error, errorInfo); // or logErrorToMyService(error, errorInfo) } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } } export default ErrorBoundary; // Usage import React from 'react'; import ErrorBoundary from './ErrorBoundary'; import MyComponent from './MyComponent'; function App() { return ( <ErrorBoundary> <MyComponent /> </ErrorBoundary> ); } export default App; """ This document provides a comprehensive set of coding standards for Redux component design. Adhering to these guidelines will improve the quality, maintainability, and performance of Redux applications.
# Testing Methodologies Standards for Redux This document outlines the recommended testing methodologies and best practices for Redux applications. It covers unit, integration, and end-to-end testing strategies specifically tailored for Redux, focusing on maintainability, performance, and accuracy. These guidelines are designed to be compatible with the latest versions of Redux and related libraries. ## 1. General Testing Principles ### 1.1. Test Pyramid * **Do This:** Adhere to the test pyramid: prioritize unit tests, then integration tests, and finally end-to-end (E2E) tests. * **Don't Do This:** Create a disproportionate number of E2E tests to cover simple logic. This makes tests slow and brittle. * **Why:** Unit tests are fast, isolate problems, and are cheap to create. Integration tests verify interactions between components, while E2E tests validate the user flow across the entire application. A well-balanced pyramid offers optimal coverage and maintainability. ### 1.2. Test-Driven Development (TDD) * **Do This:** Write your tests before or alongside writing your Redux code. Consider TDD for complex logic. * **Don't Do This:** Write tests as an afterthought or only for debugging existing issues. * **Why:** TDD ensures you’ve considered edge cases upfront, drives design, and promotes clearer, more testable code. ### 1.3. Test Coverage * **Do This:** Aim for a high test coverage (e.g., 80%+) particularly in critical areas like reducers and selectors. Use code coverage tools to identify gaps. * **Don't Do This:** Strive for 100% coverage blindly. Focus on testing the code that matters most and where bugs are most likely to occur. * **Why:** High coverage reduces risk of regressions and ensures important areas of your application are validated. ### 1.4. Testing Philosophy * **Do This:** Treat Redux logic as implementation details where appropriate. Test the behavior of your components and application, not necessarily every line of Redux code. * **Don't Do This:** Over-test implementation details, making refactoring difficult. * **Why:** End-users generally do not care if Redux is being used within the application at all. Testing the results of Redux operations as part of component tests provides sufficient confidence without overly coupling tests to the Redux implementation. ## 2. Unit Testing ### 2.1. Reducers * **Do This:** Thoroughly test each reducer for all possible actions and state transitions. Use deep equality checks to compare state objects. * **Don't Do This:** Test only the "happy path". Missing error cases or edge cases will lead to bugs. * **Why:** Reducers are at the heart of Redux. Correctly tested reducers ensure predictable state updates. * **Example:** """javascript // reducer.js const initialState = { items: [], loading: false, error: null, }; function itemsReducer(state = initialState, action) { switch (action.type) { case 'FETCH_ITEMS_REQUEST': return { ...state, loading: true, error: null }; case 'FETCH_ITEMS_SUCCESS': return { ...state, loading: false, items: action.payload }; case 'FETCH_ITEMS_FAILURE': return { ...state, loading: false, error: action.payload }; default: return state; } } export default itemsReducer; // reducer.test.js import itemsReducer from './reducer'; describe('itemsReducer', () => { const initialState = { items: [], loading: false, error: null, }; it('should return the initial state', () => { expect(itemsReducer(undefined, {})).toEqual(initialState); }); it('should handle FETCH_ITEMS_REQUEST', () => { expect(itemsReducer(initialState, { type: 'FETCH_ITEMS_REQUEST' })).toEqual({ ...initialState, loading: true, }); }); it('should handle FETCH_ITEMS_SUCCESS', () => { const payload = [{ id: 1, name: 'Item 1' }]; expect(itemsReducer(initialState, { type: 'FETCH_ITEMS_SUCCESS', payload })).toEqual({ ...initialState, items: payload, }); }); it('should handle FETCH_ITEMS_FAILURE', () => { const payload = 'Error message'; expect(itemsReducer(initialState, { type: 'FETCH_ITEMS_FAILURE', payload })).toEqual({ ...initialState, loading: false, error: payload, }); }); }); """ ### 2.2. Actions * **Do This:** Test that action creators return the correct action object with the expected type and payload. * **Don't Do This:** Over-test simple action creators. Focus on actions that perform complex logic or asynchronous operations. * **Why:** Validating action creators helps ensure that actions dispatched to the store are well-formed. * **Example:** """javascript // actions.js export const fetchItemsRequest = () => ({ type: 'FETCH_ITEMS_REQUEST', }); export const fetchItemsSuccess = (items) => ({ type: 'FETCH_ITEMS_SUCCESS', payload: items, }); export const fetchItemsFailure = (error) => ({ type: 'FETCH_ITEMS_FAILURE', payload: error, }); // actions.test.js import { fetchItemsRequest, fetchItemsSuccess, fetchItemsFailure } from './actions'; describe('actions', () => { it('should create an action to request items', () => { const expectedAction = { type: 'FETCH_ITEMS_REQUEST', }; expect(fetchItemsRequest()).toEqual(expectedAction); }); it('should create an action to receive items', () => { const items = [{ id: 1, name: 'Item 1' }]; const expectedAction = { type: 'FETCH_ITEMS_SUCCESS', payload: items, }; expect(fetchItemsSuccess(items)).toEqual(expectedAction); }); it('should create an action to handle item fetching failure', () => { const error = 'Error Message'; const expectedAction = { type: 'FETCH_ITEMS_FAILURE', payload: error, }; expect(fetchItemsFailure(error)).toEqual(expectedAction); }); }); """ ### 2.3. Selectors * **Do This:** Test selectors to ensure they correctly derive data from the state, particularly complex selectors that involve data transformations or calculations. Use "createSelector" from "reselect" for memoization and test that memoization works as expected. * **Don't Do This:** Skip testing selectors. Incorrect selectors can lead to UI bugs and performance problems. * **Why:** Selectors are a crucial layer of abstraction in Redux. Well-tested selectors guarantee data integrity and improve performance. * **Example:** """javascript // selectors.js import { createSelector } from 'reselect'; const getItems = (state) => state.items.items; const getLoading = (state) => state.items.loading; export const getVisibleItems = createSelector( [getItems, getLoading], (items, loading) => { if (loading) { return []; } return items.filter(item => item.visible); // Example filter } ); // selectors.test.js import { getVisibleItems } from './selectors'; describe('Selectors', () => { const state = { items: { items: [ { id: 1, name: 'Item 1', visible: true }, { id: 2, name: 'Item 2', visible: false }, { id: 3, name: 'Item 3', visible: true }, ], loading: false, error: null }, }; it('should return only visible items', () => { const visibleItems = getVisibleItems(state); expect(visibleItems).toEqual([ { id: 1, name: 'Item 1', visible: true }, { id: 3, name: 'Item 3', visible: true }, ]); }); }); """ ### 2.4. Middleware * **Do This:** Test middleware to confirm it intercepts and modifies actions as expected. Use mock stores to dispatch actions and assert on the side effects. * **Don't Do This:** Ignore middleware testing. Bugs in middleware can have widespread impact. * **Why:** Middleware handles side effects, logging, and other cross-cutting concerns. Thorough testing ensures these behaviors are reliable. * **Example:** """javascript // middleware.js const loggerMiddleware = (store) => (next) => (action) => { console.log('Dispatching:', action); const result = next(action); console.log('Next state:', store.getState()); return result; }; export default loggerMiddleware; // middleware.test.js import loggerMiddleware from './middleware'; import configureStore from 'redux-mock-store'; const mockStore = configureStore([loggerMiddleware]); describe('loggerMiddleware', () => { it('should log the action and next state', () => { const store = mockStore({}); const consoleSpy = jest.spyOn(console, 'log'); const action = { type: 'TEST_ACTION', payload: 'test' }; store.dispatch(action); expect(consoleSpy).toHaveBeenCalledWith('Dispatching:', action); expect(consoleSpy).toHaveBeenCalledWith('Next state:', store.getState()); consoleSpy.mockRestore(); // Clean up the spy }); }); """ ## 3. Integration Testing ### 3.1. Connected Components * **Do This:** Test that connected components correctly dispatch actions and receive props from the Redux store. Use "connect" from "react-redux" and mocking to isolate component logic. * **Don't Do This:** Rely solely on unit tests or E2E tests. Integration tests verify the crucial link between components and Redux. * **Why:** Components need to interact correctly with the store for the application to function. Integration tests validate this interaction. * **Example:** """javascript // component.js import React from 'react'; import { connect } from 'react-redux'; import { fetchItems } from './actions'; const ItemList = ({ items, fetchItems, loading, error }) => { React.useEffect(() => { fetchItems(); }, [fetchItems]); if (loading) { return <p>Loading...</p>; } if (error) { return <p>Error: {error}</p>; } return ( <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> ); }; const mapStateToProps = (state) => ({ items: state.items.items, loading: state.items.loading, error: state.items.error, }); const mapDispatchToProps = { fetchItems, }; export default connect(mapStateToProps, mapDispatchToProps)(ItemList); // component.test.js import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; // Or your preferred store setup import ItemList from './component'; import * as actions from './actions'; // Import your actions const mockStore = configureStore([]); describe('ItemList Component', () => { it('fetches and displays items correctly', async () => { const initialState = { items: { items: [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }], loading: false, error: null, }, }; const store = mockStore(initialState); // Mock the fetchItems action to avoid actual API calls jest.spyOn(actions, 'fetchItems').mockImplementation(() => ({ type: 'MOCK_FETCH_ITEMS' })); render( <Provider store={store}> <ItemList /> </Provider> ); // Verify that the items are rendered await waitFor(() => { expect(screen.getByText('Item 1')).toBeInTheDocument(); expect(screen.getByText('Item 2')).toBeInTheDocument(); }); // Verify that the action was dispatched expect(actions.fetchItems).toHaveBeenCalled(); }); it('displays a loading message while fetching items', () => { const initialState = { items: { items: [], loading: true, error: null, }, }; const store = mockStore(initialState); render( <Provider store={store}> <ItemList /> </Provider> ); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); it('displays an error message if fetching items fails', () => { const initialState = { items: { items: [], loading: false, error: 'Failed to fetch items', }, }; const store = mockStore(initialState); render( <Provider store={store}> <ItemList /> </Provider> ); expect(screen.getByText('Error: Failed to fetch items')).toBeInTheDocument(); }); }); """ ### 3.2. Asynchronous Actions (Thunks/Sagas) * **Do This:** Test that asynchronous actions (using Redux Thunk or Redux Saga) dispatch the correct actions and handle both success and error scenarios. Mock the API calls. * **Don't Do This:** Perform actual API calls during testing. This makes tests slow, unreliable, and dependent on external services. * **Why:** Asynchronous actions manage side effects. Testing them ensures that data fetching, API interactions, and other asynchronous operations function smoothly and correctly update the store. * **Example (Redux Thunk):** """javascript // actions.js export const fetchItems = () => async (dispatch) => { dispatch(fetchItemsRequest()); try { const response = await fetch('/api/items'); // Replace with actual API endpoint const data = await response.json(); dispatch(fetchItemsSuccess(data)); } catch (error) { dispatch(fetchItemsFailure(error.message)); } }; // actions.test.js import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import fetchMock from 'jest-fetch-mock'; // Or your preferred mocking library import { fetchItems, fetchItemsRequest, fetchItemsSuccess, fetchItemsFailure } from './actions'; fetchMock.enableMocks(); const mockStore = configureMockStore([thunk]); describe('async actions', () => { beforeEach(() => { fetchMock.resetMocks(); // Resets the mock before each test }); it('creates FETCH_ITEMS_SUCCESS after successfully fetching items', async () => { const items = [{ id: 1, name: 'Item 1' }]; fetchMock.mockResponseOnce(JSON.stringify(items)); const expectedActions = [ { type: 'FETCH_ITEMS_REQUEST' }, { type: 'FETCH_ITEMS_SUCCESS', payload: items }, ]; const store = mockStore({}); await store.dispatch(fetchItems()); expect(store.getActions()).toEqual(expectedActions); }); it('creates FETCH_ITEMS_FAILURE when fetching items fails', async () => { fetchMock.mockReject(new Error('Failed to fetch')); const expectedActions = [ { type: 'FETCH_ITEMS_REQUEST' }, { type: 'FETCH_ITEMS_FAILURE', payload: 'Failed to fetch' }, ]; const store = mockStore({}); await store.dispatch(fetchItems()); expect(store.getActions()).toEqual(expectedActions); }); }); """ ## 4. End-to-End (E2E) Testing ### 4.1. Scenarios * **Do This:** Focus E2E tests on critical user flows: login, checkout, submitting forms, etc. Use a tool like Cypress or Playwright. * **Don't Do This:** Overlap E2E tests with unit or integration tests. E2E tests are slow and expensive; focus on user-level validation. * **Why:** E2E tests validate the entire application stack. They find integration issues that unit and integration tests might miss. ### 4.2. State Management * **Do This:** Start each test with a clean application state. Seed the database with test data or use mock APIs for specific scenarios. * **Don't Do This:** Rely on the state from previous tests. This makes tests dependent and unreliable. * **Why:** Isolated tests are easier to debug and maintain. * **Example (Cypress - conceptual):** """javascript // cypress/e2e/item_list.cy.js describe('Item List Workflow', () => { beforeEach(() => { // Seed the database or mock the API before each test cy.intercept('/api/items', [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }]).as('getItems'); // Mock the API call cy.visit('/'); // Visit your application's home page. }); it('loads and displays the items', () => { cy.wait('@getItems'); // Wait for the API mock to fulfill cy.get('li').should('have.length', 2); cy.contains('Item 1').should('be.visible'); cy.contains('Item 2').should('be.visible'); }); it('handles errors when loading items', () => { cy.intercept('/api/items', { statusCode: 500, body: {message: 'Failed to load items'} }).as('getItemsError'); cy.visit('/'); cy.wait('@getItemsError'); // Wait for the API mock to fulfill cy.contains('Failed to load items').should('be.visible'); }); // Example interacting it('adds a new item', () => { cy.get('input[name="item_name"]').type('New Item'); cy.get('button[type="submit"]').click(); cy.contains('New Item').should('be.visible'); // Assert result }); }); """ ## 5. Best Practices & Tooling ### 5.1. Testing Libraries * **Do This:** Use modern testing libraries like: * **Jest:** A popular JavaScript testing framework with built-in mocking, assertions, and coverage reports. * **React Testing Library:** Focuses on testing components from the user's perspective. * **Redux Mock Store:** Creates mock Redux stores for testing actions and middleware. * **Cypress or Playwright:** E2E testing frameworks for browser automation. * **fetch-mock:** Mocks fetch API requests. * **Don't Do This:** Use outdated or unmaintained testing libraries. * **Why:** Using modern tools improves developer productivity and test reliability. ### 5.2. Mocking * **Do This:** Use mocking judiciously. Mock external dependencies (API calls, third-party libraries) to isolate units of code. * **Don't Do This:** Over-mock your code. Mocking too much can obscure real bugs and make tests brittle. Consider using dependency injection for better testability and less mocking. * **Why:** Mocking simplifies testing by simulating the behavior of dependencies. ### 5.3. Test Environment * **Do This:** Configure a consistent test environment (Node.js version, environment variables, etc.) Use a CI/CD pipeline to automate testing on every commit and pull request. * **Don't Do This:** Rely on local machine configurations. * **Why:** Consistent environments guarantee test reliability and prevent environment-specific bugs. ### 5.4. Async/Await * **Do This:** Use "async/await" for asynchronous tests involving promises. This makes asynchronous tests easier to read and reason about, avoiding callback hell and improving debugging. * **Don't Do This:** Rely on callbacks or ".then()" chaining for complex asynchronous test logic, as it increases the risk of errors and reduces readability. * **Why:** "async/await" simplifies asynchronous code, increasing readability and reducing errors. ### 5.5. Readable Tests * **Do This:** Write descriptive test names and clear assertions. Use comments to explain complex test logic. Follow the "AAA" pattern: Arrange, Act, Assert. * **Don't Do This:** Write cryptic or ambiguous tests that are hard to understand and maintain. * **Why:** Readable tests are easier to debug, maintain, and collaborate on. They act as living documentation for your code. By following these standards, you can ensure that your Redux applications are thoroughly tested, maintainable and reliable.
# Tooling and Ecosystem Standards for Redux This document outlines the recommended standards for tooling and the Redux ecosystem. Adhering to these standards will promote maintainability, performance, and security in Redux applications. ## 1. Recommended Libraries and Tools ### 1.1. Redux Toolkit (RTK) **Standard:** Always use Redux Toolkit (RTK) for Redux development. * **Do This:** Start new projects and refactor existing ones to use RTK. * **Don't Do This:** Manually configure Redux store, reducers, and actions without RTK. **Why:** RTK simplifies Redux development by providing utilities for common tasks, reducing boilerplate, and enforcing best practices. It handles store setup, immutable updates, and simplifies async request handling. **Code Example (Store Configuration with RTK):** """javascript // store.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; export const store = configureStore({ reducer: { counter: counterReducer, }, }); // Infer the "RootState" and "AppDispatch" types from the store itself export type RootState = ReturnType<typeof store.getState> // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} export type AppDispatch = typeof store.dispatch """ **Anti-Pattern:** Manually creating a Redux store without RTK's "configureStore". """javascript // Anti-pattern: Manual store configuration import { createStore, combineReducers } from 'redux'; import counterReducer from '../features/counter/counterSlice'; const rootReducer = combineReducers({ counter: counterReducer, }); const store = createStore(rootReducer); // Less convenient, requires more manual setup """ ### 1.2. Redux Toolkit Query (RTK Query) **Standard:** Utilize RTK Query for data fetching and caching within Redux. * **Do This:** Define API endpoints using "createApi" in RTK Query. * **Don't Do This:** Use "useEffect" or "useState" for handling data fetching directly in components, especially when the data is tightly coupled with Redux state. **Why:** RTK Query provides a streamlined approach to data fetching, automatic caching, background updates, request deduplication, and simplifies data invalidation scenarios. **Code Example (RTK Query API Definition):** """javascript // src/app/services/todos.ts import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' // Define a service using a base URL and expected endpoints export const todosApi = createApi({ reducerPath: 'todosApi', baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com' }), endpoints: (builder) => ({ getTodos: builder.query({ query: () => "/todos", }), getTodo: builder.query({ query: (todoId) => "/todos/${todoId}", }) }), }) // Export hooks for usage in functional components, which are // auto-generated based on the defined endpoints export const { useGetTodosQuery, useGetTodoQuery } = todosApi """ **Anti-Pattern:** Fetching data directly in components using "useEffect" without leveraging the benefits of RTK Query. """javascript // Anti-pattern: Manual data fetching in components import React, { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; function TodoList() { const [todos, setTodos] = useState([]); useEffect(() => { fetch('https://jsonplaceholder.typicode.com/todos') .then(response => response.json()) .then(data => setTodos(data)); }, []); return ( <ul> {todos.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); } """ ### 1.3. Reselect **Standard:** Employ Reselect for deriving data from the Redux store, particularly for complex calculations or transformations of the state, to optimize performance. * **Do This:** Create memoized selectors using "createSelector" from Reselect. * **Don't Do This:** Perform complex data transformations directly in components. **Why:** Reselect provides memoization capabilities, ensuring that derived data is only recalculated when its dependencies change, thus optimizing performance by avoiding unnecessary re-renders. **Code Example (Reselect Selector):** """javascript import { createSelector } from 'reselect'; const selectTodos = state => state.todos; const selectFilter = state => state.filter; const selectVisibleTodos = createSelector( [selectTodos, selectFilter], (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos; case 'SHOW_COMPLETED': return todos.filter(todo => todo.completed); case 'SHOW_ACTIVE': return todos.filter(todo => !todo.completed); default: return todos; } } ); export default selectVisibleTodos; """ **Anti-Pattern:** Performing filtering logic within the React component itself, leading to unnecessary re-renders. """javascript // Anti-pattern: Filtering in the component import { useSelector } from 'react-redux'; function TodoList() { const todos = useSelector(state => state.todos); const filter = useSelector(state => state.filter); const visibleTodos = () => { switch (filter) { case 'SHOW_ALL': return todos; case 'SHOW_COMPLETED': return todos.filter(todo => todo.completed); case 'SHOW_ACTIVE': return todos.filter(todo => !todo.completed); default: return todos; } }; return ( <ul> {visibleTodos().map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); } """ ### 1.4. Redux DevTools **Standard:** Always enable Redux DevTools in development environments. * **Do This:** Integrate the "Redux DevTools Extension" into your store configuration. * **Don't Do This:** Deploy production builds with Redux DevTools enabled. **Why:** Redux DevTools provide powerful debugging capabilities, allowing developers to inspect state changes, dispatch actions, and time travel to previous states, greatly improving the debugging process. **Code Example (Enabling Redux DevTools with RTK):** """javascript import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; export const store = configureStore({ reducer: { counter: counterReducer, }, devTools: process.env.NODE_ENV !== 'production', // Enable only in development }); """ **Anti-Pattern:** Omitting the "devTools" configuration or accidentally enabling it in production builds. ### 1.5. Immer **Standard:** While Redux Toolkit comes with Immer integrated, understand **how** Immer allows for mutable state updates inside reducers while ensuring immutability under the hood. If not using RTK directly, consider explicit import with the correct configuration. * **Do This:** Mutate the "state" object directly within RTK reducers. * **Don't Do This:** Manually create new state objects for simple updates when using Immer. **Why:** Immer simplifies reducer logic by allowing developers to write mutable-style code, which is then automatically converted into immutable updates, reducing boilerplate and improving readability. **Code Example (Immer with RTK Slice):** """javascript import { createSlice } from '@reduxjs/toolkit'; const initialState = { value: 0, }; export const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { state.value += 1; // Direct mutation with Immer }, decrement: (state) => { state.value -= 1; // Direct mutation with Immer }, incrementByAmount: (state, action) => { state.value += action.payload; // Direct mutation with Immer }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export default counterSlice.reducer; """ **Anti-Pattern:** Manually handling immutability within RTK slices, negating Immer's benefits. """javascript // Anti-pattern: Manual immutability handling in RTK import { createSlice } from '@reduxjs/toolkit'; const initialState = { value: 0, }; export const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { return { ...state, value: state.value + 1 }; // Unnecessary manual immutability }, }, }); export const { increment } = counterSlice.actions; export default counterSlice.reducer; """ ## 2. Ecosystem Patterns and Best Practices ### 2.1. Feature Slices **Standard:** Organize related Redux logic (reducers, actions, selectors) into feature slices. * **Do This:** Create separate slice files for each feature area (e.g., "usersSlice.js", "productsSlice.js"). * **Don't Do This:** Lump all Redux logic into a single giant file. **Why:** Feature slices improve code organization, maintainability, and scalability by encapsulating related state and logic within a single module. **Code Example (Feature Slice):** """javascript // features/users/usersSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { fetchUsers } from '../../api'; export const fetchUsersAsync = createAsyncThunk( 'users/fetchUsers', async () => { const response = await fetchUsers(); return response.data; } ); const initialState = { users: [], status: 'idle', error: null, }; const usersSlice = createSlice({ name: 'users', initialState, reducers: { // Synchronous reducers here. }, extraReducers: (builder) => { builder .addCase(fetchUsersAsync.pending, (state) => { state.status = 'loading'; }) .addCase(fetchUsersAsync.fulfilled, (state, action) => { state.status = 'succeeded'; state.users = action.payload; }) .addCase(fetchUsersAsync.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }); }, }); export default usersSlice.reducer; """ **Anti-Pattern:** Colocating the entire app state into one big reducer file. ### 2.2. Asynchronous Actions with "createAsyncThunk" **Standard:** Handle asynchronous logic using "createAsyncThunk" from Redux Toolkit. * **Do This:** Define asynchronous operations (e.g., API calls) using "createAsyncThunk". * **Don't Do This:** Manually create thunks or use other middleware for asynchronous actions such as "redux-promise". **Why:** "createAsyncThunk" simplifies asynchronous action creation, automatically generates action types for pending, fulfilled, and rejected states, and integrates seamlessly with RTK slices. **Code Example (Async Thunk):** """javascript // features/todos/todosSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; export const fetchTodos = createAsyncThunk( 'todos/fetchTodos', async () => { const response = await fetch('https://jsonplaceholder.typicode.com/todos'); const data = await response.json(); return data; } ); const initialState = { todos: [], status: 'idle', error: null, }; const todosSlice = createSlice({ name: 'todos', initialState, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchTodos.pending, (state) => { state.status = 'loading'; }) .addCase(fetchTodos.fulfilled, (state, action) => { state.status = 'succeeded'; state.todos = action.payload; }) .addCase(fetchTodos.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }); }, }); export default todosSlice.reducer; """ **Anti-Pattern:** Manually creating thunks without "createAsyncThunk". """javascript // Anti-pattern: Manual thunk creation export const fetchTodos = () => async (dispatch) => { dispatch({ type: 'todos/fetchTodos/pending' }); try { const response = await fetch('https://jsonplaceholder.typicode.com/todos'); const data = await response.json(); dispatch({ type: 'todos/fetchTodos/fulfilled', payload: data }); } catch (error) { dispatch({ type: 'todos/fetchTodos/rejected', error: error.message }); } }; """ ### 2.3. Selectors for Data Retrieval **Standard:** Always use selectors to retrieve data from the Redux store. * **Do This:** Create selector functions to access specific parts of the state. * **Don't Do This:** Directly access state properties in components. **Why:** Selectors provide several benefits, including: * **Abstraction:** Hide the structure of the state from the components. This makes it easier to refactor the state without affecting components. * **Memoization:** Avoid unnecessary re-renders by memoizing the results of the selector functions. * **Testability:** Make it easier to test the data retrieval logic. **Code Example (Selector within a Slice):** """javascript // features/todos/todosSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; const initialState = { todos: [], status: 'idle', error: null, }; const todosSlice = createSlice({ name: 'todos', initialState, reducers: {}, extraReducers: (builder) => { //... }, }); export const selectAllTodos = (state) => state.todos.todos; // Selector function export const selectTodoById = (state, todoId) => state.todos.todos.find(todo => todo.id === todoId); export default todosSlice.reducer; """ And in a component: """javascript import { useSelector } from 'react-redux'; import { selectAllTodos } from './todosSlice'; function TodoList() { const todos = useSelector(selectAllTodos); return ( <ul> {todos.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); } """ **Anti-Pattern:** Access the Redux state directly within a React component instead of using a selector. ## 3. Configuration and Setup ### 3.1. Store Configuration **Standard:** Configure the Redux store using "configureStore" from Redux Toolkit. * **Do This:** Use the "configureStore" utility with reducers. * **Don't Do This:** Create store manually using "createStore" and middleware manually. **Why:** The "configureStore" function simplifies store setup by automatically including necessary middleware and enabling Redux DevTools. **Code Example (Store Configuration with multiple slices):** """javascript import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; import todosReducer from '../features/todos/todosSlice'; import { todosApi } from './services/todos' export const store = configureStore({ reducer: { counter: counterReducer, todos: todosReducer, [todosApi.reducerPath]: todosApi.reducer, // RTK Query }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(todosApi.middleware), }); // Infer the "RootState" and "AppDispatch" types from the store itself export type RootState = ReturnType<typeof store.getState> // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} export type AppDispatch = typeof store.dispatch """ **Anti-Pattern:** Skip adding the middleware to your store configuration when using RTK Query, which will prevent usage of its features. ### 3.2. TypeScript Integration (strongly recommended) **Standard:** Use TypeScript with Redux to ensure type safety. * **Do This:** Define types for states, actions, and selectors using TypeScript. This includes extracting types for "RootState" and "AppDispatch" from the store. * **Don't Do This:** Use "any" type excessively or rely on JavaScript without type checking. **Why:** TypeScript can help catch errors early in development, improving code quality, maintainability, and developer experience. **Code Example (TypeScript with RTK):** """typescript // features/counter/counterSlice.ts import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from '../../app/store' // Define a type for the slice state interface CounterState { value: number } // Define the initial state using that type const initialState: CounterState = { value: 0, } export const counterSlice = createSlice({ name: 'counter', // "createSlice" will infer the state type from the "initialState" argument initialState, reducers: { increment: (state) => { state.value += 1 }, decrement: (state) => { state.value -= 1 }, incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload }, }, }) export const { increment, decrement, incrementByAmount } = counterSlice.actions // Other code such as selectors can use the imported "RootState" type export const selectCount = (state: RootState) => state.counter.value export default counterSlice.reducer """ ### 3.3 Normalizing Data **Standard:** Use normalized state structures, when dealing with complex, relational data. * **Do This:** Structure data as objects keyed by unique IDs. * **Don't Do This:** Store data as arrays without unique identifiers. **Why:** Normalizing data simplifies state updates, reduces data duplication, and improves performance when dealing with large datasets. **Code Example (Normalizing Data):** """javascript // Example of normalized state const initialState = { entities: { 1: { id: 1, title: 'Todo 1', completed: false }, 2: { id: 2, title: 'Todo 2', completed: true }, }, ids: [1, 2], }; // Example of denormalized state (anti-pattern) const initialStateDenormalized = [ { id: 1, title: 'Todo 1', completed: false }, { id: 2, title: 'Todo 2', completed: true }, ]; """ Using a library like "normalizr" is encouraged for complex normalization scenarios. ### 3.4. Immutable Data Handling **Standard:** (Already covered by Immer and RTK usage) Enforce immutability when updating the Redux store. * **Do This:** Rely on Immer's capabilities within RTK reducers. * **Don't Do This:** Mutate the state directly without using Immer or other immutable update techniques. **Why:** Immutability prevents unexpected side effects, simplifies debugging, and enables efficient change detection in React components. ## 4. Error Handling ### 4.1. Handling Async Errors **Standard:** Handle errors in asynchronous actions and update the state accordingly. * **Do This:** Use "try...catch" blocks inside "createAsyncThunk" callbacks and dispatch error actions to update the state. * **Don't Do This:** Ignore errors or leave the state in an inconsistent state. **Why:** Proper error handling ensures that the application can gracefully recover from errors and provide informative messages to the user. **Code Example (Error Handling in "createAsyncThunk"):** """javascript // features/todos/todosSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; export const fetchTodos = createAsyncThunk( 'todos/fetchTodos', async () => { try { const response = await fetch('https://jsonplaceholder.typicode.com/todos'); const data = await response.json(); return data; } catch (error) { // Manually return a rejected promise with the error return thunkAPI.rejectWithValue(error.message); } } ); const initialState = { todos: [], status: 'idle', error: null, }; const todosSlice = createSlice({ //...previous code extraReducers: (builder) => { builder //...previous code .addCase(fetchTodos.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message || action.payload; }); }, }); """ ### 4.2. Centralized Error Handling **Standard:** Implement a centralized error handling mechanism, such as a global error boundary or error reporting service, to capture and report errors that occur within the Redux store. **Why:** Centralized error handling provides a consistent way to handle errors across the application, simplifies debugging, and reduces code duplication. This can involve middleware that listens for error actions and reports them to a service like Sentry or Bugsnag. For React components, a higher-order component or error boundary can be used. By adhering to these tooling and ecosystem standards, Redux applications will be more maintainable, performant, and robust. This document serves as a guide for developers and AI coding assistants to follow best practices in Redux development.
# State Management Standards for Redux This document outlines the coding standards for state management in Redux applications. It provides guidelines and best practices for managing application state, data flow, and reactivity, ensuring maintainability, performance, and consistency across the codebase. These standards are designed to be used by developers and AI coding assistants alike. ## 1. Core Principles ### 1.1. Single Source of Truth * **Standard:** Maintain a single, centralized store as the single source of truth for your application's state. * **Why:** Ensures consistency, simplifies debugging, and facilitates features like time-travel debugging. Avoid duplicating state across multiple components or modules. * **Do This:** """javascript // store.js import { configureStore } from '@reduxjs/toolkit'; import rootReducer from './rootReducer'; const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware(), // Include default middleware devTools: process.env.NODE_ENV !== 'production', // Enable Redux DevTools in development }); export default store; """ * **Don't Do This:** """javascript // Avoid keeping independent state copies let componentState = { data: [] }; // In a component """ ### 1.2. Immutability * **Standard:** Treat state as immutable. Never directly modify the state; instead, create new copies with updated values. * **Why:** Enables efficient change detection, supports time-travel debugging, and prevents unexpected side effects. * **Do This:** """javascript // Correct usage in a reducer function. const initialState = { items: [] }; function itemsReducer(state = initialState, action) { switch (action.type) { case 'ADD_ITEM': return { ...state, items: [...state.items, action.payload] }; // Create a new array default: return state; } } export default itemsReducer; """ * **Don't Do This:** """javascript // Incorrect - directly modifying the state. function itemsReducer(state = initialState, action) { switch (action.type) { case 'ADD_ITEM': state.items.push(action.payload); // Directly modifies the existing array return state; default: return state; } } export default itemsReducer; """ * **Tools:** Use libraries like Immer or "@reduxjs/toolkit" to simplify immutable state management. "@reduxjs/toolkit" is generally preferred now for most Redux projects. ### 1.3. Predictable State Updates * **Standard:** Ensure that state updates are predictable and follow a unidirectional data flow (Action -> Reducer -> Store -> View). * **Why:** Makes it easier to understand how state changes over time, reduces debugging complexity, and allows for more reliable application behavior. * **Do This:** Dispatch actions that describe *what* happened, not *how* to change the state. Let the reducers determine how to update the state based on the action. * **Don't Do This:** Dispatch actions that directly mutate state. Keep the reducer logic pure and predictable. ## 2. Redux Toolkit (RTK) ### 2.1. Adoption of RTK * **Standard:** Use Redux Toolkit (RTK) as the primary tool for Redux development. RTK simplifies Redux development, reduces boilerplate code, and promotes best practices. * **Why:** Offers "configureStore", "createSlice", "createAsyncThunk", and other utilities that streamline common Redux tasks. * **Do This:** """javascript // Creating a slice using createSlice import { createSlice } from '@reduxjs/toolkit'; const initialState = { value: 0, status: 'idle', }; const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, }, extraReducers: (builder) => { builder .addCase(incrementAsync.pending, (state) => { state.status = 'loading'; }) .addCase(incrementAsync.fulfilled, (state, action) => { state.status = 'idle'; state.value += action.payload; }); }, }); export const { increment, decrement } = counterSlice.actions; export const selectCount = (state) => state.counter.value; // Example selector export default counterSlice.reducer; """ * **Don't Do This:** Manually create actions, action types, and reducers separately without using RTK's "createSlice". ### 2.2. Async Thunks * **Standard:** For asynchronous operations, use "createAsyncThunk" provided by Redux Toolkit. * **Why:** Simplifies handling asynchronous logic, reduces boilerplate, and provides standardized action types for pending, fulfilled, and rejected states. * **Do This:** """javascript // Using createAsyncThunk import { createAsyncThunk } from '@reduxjs/toolkit'; import { increment } from './counterSlice'; export const incrementAsync = createAsyncThunk( 'counter/fetchCount', async (amount) => { await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call return amount } ); """ * **Don't Do This:** Manually create separate actions and reducers for asynchronous operations. ### 2.3. Configure Store * **Standard:** Use "configureStore" from RTK to set up the Redux store. * **Why:** Simplifies store setup, automatically includes essential middleware (like "redux-thunk"), and provides a convenient way to configure the store with reducers and middleware. * **Do This:** """javascript // Configuring the store with configureStore import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './counterSlice'; const store = configureStore({ reducer: { counter: counterReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware(), devTools: process.env.NODE_ENV !== 'production', }); export default store; """ * **Don't Do This:** Manually apply enhancers and middleware without using "configureStore". ## 3. Reducer Structure and Organization ### 3.1. Feature-Based Slices * **Standard:** Organize reducers into feature-based slices. A slice represents a self-contained part of the application state and its associated logic. * **Why:** Improves modularity, maintainability, and code organization. * **Do This:** """javascript // Separate slices for different features // features/users/userSlice.js import { createSlice } from '@reduxjs/toolkit'; const initialState = { users: [], status: 'idle', }; const userSlice = createSlice({ name: 'users', initialState, reducers: { addUser: (state, action) => { state.users.push(action.payload); }, // ... other user-related reducers }, }); export const { addUser } = userSlice.actions; export default userSlice.reducer; """ """javascript // features/posts/postSlice.js import { createSlice } from '@reduxjs/toolkit'; const initialState = { posts: [], status: 'idle', }; const postSlice = createSlice({ name: 'posts', initialState, reducers: { addPost: (state, action) => { state.posts.push(action.payload); }, // ... other post-related reducers }, }); export const { addPost } = postSlice.actions; export default postSlice.reducer; """ * **Don't Do This:** Lump all reducer logic into a single, monolithic reducer. ### 3.2. Root Reducer * **Standard:** Combine all slice reducers into a single root reducer using "combineReducers" or let "configureStore" handle it. * **Why:** Provides a single entry point for the Redux store. * **Do This:** """javascript // rootReducer.js import { combineReducers } from 'redux'; import userReducer from './features/users/userSlice'; import postReducer from './features/posts/postSlice'; const rootReducer = combineReducers({ users: userReducer, posts: postReducer, }); export default rootReducer; """ * **Don't Do This:** Directly pass individual slice reducers to the store without combining them. ### 3.3. Normalize State * **Standard:** Normalize nested or relational data within the state. Store entities in a flat, keyed object, and reference them by IDs. * **Why:** Improves performance when updating or accessing specific entities, reduces data duplication and keeps the state predictable. Simplifies updates to deeply nested data. * **Do This:** """javascript // Normalized state for a list of users const initialState = { users: { byId: { '1': { id: '1', name: 'John Doe' }, '2': { id: '2', name: 'Jane Smith' }, }, allIds: ['1', '2'], }, }; """ * **Don't Do This:** Directly store nested and duplicated data in the state. ## 4. Actions and Action Creators ### 4.1. Descriptive Action Types * **Standard:** Use descriptive and meaningful action types. Action types should clearly indicate the intent of the action. * **Why:** Improves readability, debuggability, and maintainability. * **Do This:** """javascript // Descriptive action types const USER_ADDED = 'users/USER_ADDED'; const POST_UPDATED = 'posts/POST_UPDATED'; """ * **Don't Do This:** Use generic or ambiguous action types like "UPDATE_DATA" or "SET_VALUE". ### 4.2. Action Creators * **Standard:** Use action creators to encapsulate the process of creating actions. Action creators should return a plain JavaScript object with a "type" property and any necessary payload. * **Why:** Provides a consistent and reusable way to create actions, simplifies testing, and allows for more complex action creation logic. RTK's "createSlice" automatically generates action creators. * **Do This:** """javascript // Action creator (generated by slice) import { addUser } from './userSlice'; // Example action creator """ * **Don't Do This:** Directly dispatch action objects without using action creators. ### 4.3. Payload Convention * **Standard:** Use a consistent payload convention for actions. For most actions, the payload should be a single object with properties that describe the data being passed. * **Why:** Improves consistency and makes it easier to access data within reducers. * **Do This:** """javascript // Action with payload const user = { id: '3', name: 'Alice' }; dispatch(addUser(user)); """ * **Don't Do This:** Pass multiple arguments or inconsistent payload structures. ## 5. Selectors ### 5.1. Definition and Usage * **Standard:** Use selectors to extract specific pieces of data from the Redux store. Create them using "createSelector" from "reselect" library. * **Why:** Improves performance by memoizing selector results, encapsulates state structure, and makes it easier to update the state structure without affecting components. * **Do This:** """javascript // Selectors using reselect - but often defined inside a slice. import { createSelector } from '@reduxjs/toolkit'; const selectUsers = (state) => state.users; const selectAllUserNames = createSelector( [selectUsers], (users) => users.map((user) => user.name) ); export default selectAllUserNames; //In component import { useSelector } from 'react-redux'; import selectAllUserNames from './selectAllUserNames'; const UserList = () => { const userNames = useSelector(selectAllUserNames); return ( <ul> {userNames.map((name) => ( <li key={name}>{name}</li> ))} </ul> ); }; export default UserList; """ * **Don't Do This:** Directly access state properties within components without using selectors. ### 5.2. Memoization * **Standard:** Utilize memoization features provided by "reselect" to prevent unnecessary re-renders. Make good use of derived data. * **Why:** Improves performance by preventing components from re-rendering when the data they depend on has not changed. * **Do This:** Create selectors using "createSelector" from "reselect". * **Don't Do This:** Write selectors by hand without memoization, or by passing the entire state object to the selector function. ### 5.3. Location * **Standard:** Define selectors within the corresponding slice file. This keeps selectors close to the state they operate on and improves code organization. * **Why:** Enhances modularity and maintainability. * **Do This:** """javascript // postSlice.js import { createSlice, createSelector } from '@reduxjs/toolkit'; const postSlice = createSlice({ //...rest of code here }); const selectPosts = (state) => state.posts.posts; export const selectAllPosts = createSelector( [selectPosts], (posts) => posts ); """ * **Don't Do This:** Define selectors in unrelated files or component files. ## 6. Middleware ### 6.1. Custom Middleware * **Standard:** Create custom middleware only when necessary. Middleware should perform tasks such as logging, analytics, or handling side effects. * **Why:** Provides a centralized way to handle cross-cutting concerns, improves code organization, and reduces boilerplate. * **Do This:** """javascript // Custom middleware for logging actions const loggerMiddleware = store => next => action => { console.log('Dispatching:', action); let result = next(action); console.log('Next state:', store.getState()); return result; }; """ * **Don't Do This:** Overuse middleware or perform business logic within middleware. ### 6.2. Middleware Ordering * **Standard:** Arrange middleware in a logical order. Logging and error-handling middleware should come first, followed by middleware that modifies actions or state. * **Why:** Affects the order in which middleware are executed, which can impact application behavior. "configureStore" handles this ordering implicitly. * **Do This:** Use the "getDefaultMiddleware" option in "configureStore" to include standard middleware. * **Don't Do This:** Randomly order middleware without considering their dependencies. ### 6.3. Thunks as Middleware * **Standard:** Redux Thunk should primarily be used for handling asynchronous logic and side effects. Use "createAsyncThunk" over manually creating thunks. * **Why:** Provides a clean and testable way to handle asynchronous operations, reduces boilerplate code. * **Do This:** RTK's configureStore includes thunk middleware by default. * **Don't Do This:** Dispatch multiple actions from within components or manually manage asynchronous logic outside of thunks. ## 7. Data Fetching ### 7.1. Where to Fetch Data * **Standard:** Centralize data fetching logic within action creators or thunks. Avoid fetching data directly within components. * **Why:** Keeps components focused on presentation, improves testability, and provides a single place to manage data fetching logic. * **Do This:** """javascript // Fetching data in an async thunk import { createAsyncThunk } from '@reduxjs/toolkit'; import { setUsers } from './userSlice'; export const fetchUsers = createAsyncThunk( 'users/fetchUsers', async () => { const response = await fetch('/api/users'); const data = await response.json(); return data; } ); """ * **Don't Do This:** Fetch data directly within components using "useEffect" or other lifecycle methods. ### 7.2. Handling Loading States * **Standard:** Use Redux state to manage loading and error states. Display loading indicators and error messages in the UI based on the state. * **Why:** Provides a consistent and predictable way to handle asynchronous operations. This is automatically handled via RTK's "createAsyncThunk". * **Do This:** See "createAsyncThunk" example under section 2.2. * **Don't Do This:** Manage loading and error states locally within components. ## 8. Performance ### 8.1. Minimize State Updates * **Standard:** Minimize the number of state updates. Update only the necessary parts of the state. * **Why:** Reduces unnecessary re-renders and improves performance, especially in complex applications. * **Do This:** Use targeted actions and reducers to update specific parts of the state. * **Don't Do This:** Update the entire state object unnecessarily. ### 8.2. Batch Updates * **Standard:** Batch multiple updates into a single state update using libraries like "redux-batched-actions" or by combining multiple actions within a thunk. * **Why:** Improves performance by reducing the number of re-renders. * **Do This:** """javascript // Batching updates using redux-batched-actions (less common with RTK) import { batch } from 'react-redux'; import { updateName, updateAge } from './userSlice'; const updateUser = (name, age) => (dispatch) => { batch(() => { dispatch(updateName(name)); dispatch(updateAge(age)); }); }; """ * **Don't Do This:** Dispatch multiple actions sequentially without batching them. ### 8.3. Reselect * **Standard:** Use Reselect library to create memoized selectors for derived data. * **Why:** Improves performance by preventing unnecessary re-renders when the derived data has not changed. * **Do This:** See Section 5 for guidance related to Reselect. * **Don't Do This:** Directly derive data within components or selectors without using memoization. ## 9. Testing ### 9.1. Unit Testing Reducers * **Standard:** Write unit tests for reducers to ensure they correctly update the state in response to actions. * **Why:** Ensures that reducers behave as expected and prevents regressions. * **Do This:** """javascript // Unit test for a reducer import reducer, { addUser } from './userSlice'; describe('userSlice reducer', () => { const initialState = { users: [], status: 'idle', }; it('should handle addUser', () => { const user = { id: '1', name: 'John Doe' }; const nextState = reducer(initialState, addUser(user)); expect(nextState.users).toEqual([user]); }); }); """ * **Don't Do This:** Skip writing unit tests for reducers. ### 9.2. Testing Actions and Thunks * **Standard:** Write unit tests for actions and thunks to ensure they dispatch the correct actions and handle asynchronous logic properly. * **Why:** Ensures that actions and thunks behave as expected and prevents regressions. * **Do This:** """javascript // Unit test for an async thunk import { fetchUsers } from './userSlice'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import fetchMock from 'jest-fetch-mock'; fetchMock.enableMocks(); const mockStore = configureStore([thunk]); describe('userSlice async thunks', () => { beforeEach(() => { fetchMock.resetMocks(); }); it('should handle fetchUsers', async () => { const users = [{ id: '1', name: 'John Doe' }]; fetchMock.mockResponseOnce(JSON.stringify(users)); const store = mockStore({}); await store.dispatch(fetchUsers()); const actions = store.getActions(); expect(actions[0].type).toEqual('users/fetchUsers/pending'); expect(actions[1].type).toEqual('users/fetchUsers/fulfilled'); expect(actions[1].payload).toEqual(users); }); }); """ * **Don't Do This:** Skip writing unit tests for actions and thunks. ## 10. Security ### 10.1. Prevent State Tampering * **Standard:** Protect the Redux store from client-side tampering. Do not store sensitive data in the Redux store that should not be exposed to the client. * **Why:** Prevents malicious users from modifying the application state and potentially compromising security. * **Do This:** Limit the amount of sensitive data stored in the Redux store. Only store data that is necessary for the client-side rendering and interaction of the application. Validate and sanitize user inputs before storing them in the Redux store. Avoid storing raw HTML or JavaScript code. * **Don't Do This:** Store sensitive information in the Redux store. Directly use user inputs without validation and sanitization. Store raw HTML or JavaScript code in the Redux store. ### 10.2. Data Sanitization * **Standard:** Sanitize and validate user inputs before storing them in the Redux store. * **Why:** Prevent cross-site scripting (XSS) attacks and other security vulnerabilities. * **Do This:** Use appropriate sanitization and validation libraries to clean user inputs. * **Don't Do This:** Directly store user inputs without sanitization and validation. ### 10.3. Secure Data Transmission * **Standard:** Use HTTPS for all data transmission between the client and server. * **Why:** Protects data from eavesdropping and tampering. * **Do This:** Ensure that the application is served over HTTPS. * **Don't Do This:** Transmit sensitive data over HTTP. ## 11. Code Formatting and Style ### 11.1. Consistent Formatting * **Standard:** Use a consistent code formatting style (e.g., Prettier) to improve readability and maintainability. * **Why:** Makes code easier to read, understand, and maintain. * **Do This:** Configure Prettier to automatically format code on save. * **Don't Do This:** Use inconsistent code formatting styles. ### 11.2. Meaningful Comments * **Standard:** Write meaningful comments to explain complex or non-obvious code. * **Why:** Improves code understanding and maintainability. * **Do This:** Comment complex logic, algorithms, or non-obvious code sections. Add JSDoc comments to functions and components. * **Don't Do This:** Write unnecessary or redundant comments. ### 11.3. Naming Conventions * **Standard:** Follow consistent naming conventions for variables, functions, and components. * **Why:** Improves code readability and maintainability. * **Do This:** Use camelCase for variables and functions, PascalCase for components, and uppercase with underscores for constants: """javascript // Naming conventions const userName = 'John Doe'; // Variable function calculateTotal() { } // Function const UserProfile = () => { }; // Component const API_URL = '/api/users'; // Constant """ * **Don't Do This:** Use inconsistent or ambiguous naming conventions.
# Security Best Practices Standards for Redux This document outlines security best practices for Redux applications. It provides guidelines to help developers build secure and robust applications, mitigate common vulnerabilities, and adopt secure coding patterns specific to Redux. It is based on the latest Redux Toolkit, Redux core and ecosystem functionalities. ## 1. General Security Principles in Redux ### 1.1. Data Sanitization and Validation **Standard:** Sanitize and validate all user inputs before storing them in the Redux store or using them in actions and reducers. **Why:** Prevents injection attacks (e.g., XSS) and ensures data integrity. **Do This:** * Use validation libraries like "Joi", "Yup", or "validator.js". * Sanitize data by encoding or stripping potentially harmful characters. * Perform validation both on the client-side and the server-side. **Don't Do This:** * Directly use unsanitized user input in Redux actions or the store. * Rely solely on client-side validation, as it can be bypassed. **Code Example:** """javascript // Using Yup for validation import * as Yup from 'yup'; const schema = Yup.object().shape({ name: Yup.string().required().min(2).max(50), email: Yup.string().email().required(), }); const submitForm = async (data) => { try { const validatedData = await schema.validate(data, { abortEarly: false }); // Dispatch a Redux action with validatedData store.dispatch(submitAction(validatedData)); } catch (error) { // Handle validation errors console.error("Validation error:", error.errors); } }; """ ### 1.2. Authentication and Authorization **Standard:** Implement secure authentication and authorization mechanisms to protect sensitive data in the Redux store. **Why:** Ensures that only authorized users can access and modify data. **Do This:** * Use secure authentication protocols like OAuth 2.0 or JWT. * Store user roles and permissions in the Redux store for authorization checks. * Implement middleware to intercept actions and verify user permissions. **Don't Do This:** * Store sensitive authentication tokens directly in the Redux store. * Implement authorization checks solely on the client-side. **Code Example:** """javascript // Redux middleware for authorization const checkPermission = (requiredRole) => (store) => (next) => (action) => { const user = store.getState().auth.user; if (user && user.role === requiredRole) { return next(action); } else { console.warn("Unauthorized action:", action.type); // Optionally dispatch an error action to the store store.dispatch({ type: 'PERMISSION_DENIED', payload: action.type }); return; // terminate, don't pass action on } }; // applying middleware (using Redux Toolkit configureStore) import { configureStore } from '@reduxjs/toolkit' const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(checkPermission('admin')) }) """ ### 1.3. Encryption of Sensitive Data **Standard:** Encrypt sensitive data stored in the Redux store and during transmission. **Why:** Protects data from eavesdropping or unauthorized access. **Do This:** * Encrypt data at rest using libraries like "crypto-js". * Use HTTPS for all network requests to encrypt data in transit. * Consider encrypting sensitive parts of Redux store. **Don't Do This:** * Store sensitive information in plain text. * Transmit sensitive data over unencrypted connections (HTTP). **Code Example:** """javascript // Encrypting data before storing it in the Redux store import CryptoJS from 'crypto-js'; const encryptionKey = 'YOUR_ENCRYPTION_KEY'; // Store securely, e.g., in environment variables const encrypt = (data) => { return CryptoJS.AES.encrypt(JSON.stringify(data), encryptionKey).toString(); }; const decrypt = (encryptedData) => { const bytes = CryptoJS.AES.decrypt(encryptedData, encryptionKey); return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); }; // Reducer example: const initialState = { encryptedSensitiveData: encrypt({ ssn: '123-456-7890' }) }; const sensitiveReducer = (state = initialState, action) => { switch (action.type) { case 'UPDATE_SENSITIVE_DATA': return { ...state, encryptedSensitiveData: encrypt(action.payload) }; case 'GET_SENSITIVE_DATA': return { ...state, decryptedData: decrypt(state.encryptedSensitiveData) }; default: return state; } }; """ ### 1.4. Preventing State Injection **Standard:** Validate the structure and types of data received in actions to prevent malicious state injection. **Why:** Prevents attackers from injecting arbitrary data into the Redux store, potentially compromising the application. Always favor explicitly updating parts of the store with specific data, rather than wholesale replacement. **Do This:** * Define action creators with strict type checking and validation. * Use TypeScript or PropTypes to enforce data types. * Validate the structure of data received in reducers before updating the state. **Don't Do This:** * Directly merge untrusted data into the Redux store without validation. * Allow actions to replace entire slices of the state arbitrarily. **Code Example:** """typescript // Using TypeScript to validate action payloads interface UpdateUserAction { type: 'UPDATE_USER'; payload: { id: string; name: string; email: string; }; } const updateUser = (payload: UpdateUserAction['payload']): UpdateUserAction => ({ type: 'UPDATE_USER', payload, }); // Reducer interface UserState { id: string; name: string; email: string; } const initialState: UserState = { id: '', name: '', email: '', }; const userReducer = (state = initialState, action: UpdateUserAction): UserState => { switch (action.type) { case 'UPDATE_USER': // Validate the structure of the payload if (typeof action.payload.id !== 'string' || typeof action.payload.name !== 'string' || typeof action.payload.email !== 'string') { console.error('Invalid payload for UPDATE_USER action'); return state; } return { ...state, id: action.payload.id, name: action.payload.name, email: action.payload.email, }; default: return state; } }; """ ### 1.5. Secure Storage of Secrets **Standard:** Do not store secrets such as API keys, encryption keys, or database passwords directly in the Redux store or client-side code. **Why:** Prevents exposure of sensitive credentials. **Do This:** * Store secrets in secure configuration files. * Retrieve secrets from environment variables. * Use a secrets management service (e.g., AWS Secrets Manager, HashiCorp Vault) for centralized storage and retrieval. **Don't Do This:** * Hardcode secrets in the Redux store or client-side code. * Commit secrets to version control. **Code Example:** """javascript // Retrieving API key from environment variables const apiKey = process.env.REACT_APP_API_KEY; // Using the API key in a Redux action import { createAsyncThunk } from '@reduxjs/toolkit'; export const fetchData = createAsyncThunk( 'data/fetchData', async (arg, thunkAPI) => { try { if (!apiKey) { console.error("API Key missing!"); return thunkAPI.rejectWithValue("Api Key missing") } const response = await fetch("/api/data?apiKey=${apiKey}"); const data = await response.json(); return data; } catch (error) { return thunkAPI.rejectWithValue(error.message); } } ); """ ## 2. Redux Specific Security Considerations ### 2.1. Middleware Security Checks **Standard:** Use Redux middleware to implement security checks and enforce policies before actions reach the reducers. **Why:** Provides a centralized point for validating and sanitizing actions, preventing unauthorized state modifications. **Do This:** * Implement middleware to check user permissions, validate action payloads, and sanitize input data. * Log security-related events for auditing and monitoring. * Avoid complex logic within reducers; delegate to middleware. **Don't Do This:** * Perform security checks solely within reducers. * Allow actions with invalid or malicious payloads to reach the reducers. **Code Example:** """javascript // Redux middleware for payload validation const validatePayload = (schema) => (store) => (next) => (action) => { try { schema.validateSync(action.payload, { abortEarly: false }); return next(action); } catch (error) { console.error("Invalid action payload:", action.type, error.errors); // Dispatch an error action store.dispatch({ type: 'INVALID_PAYLOAD', payload: { actionType: action.type, errors: error.errors } }); return; // stop processing this action } }; // Example schema using Yup import * as Yup from 'yup'; const mySchema = Yup.object().shape({ field1: Yup.string().required(), field2: Yup.number().positive() }); // applying middleware (using Redux Toolkit configureStore) import { configureStore } from '@reduxjs/toolkit' const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(validatePayload(mySchema)) }) """ ### 2.2. Immutability and Data Integrity **Standard:** Enforce immutability of the Redux store and action payloads to prevent accidental or malicious data modifications. **Why:** Ensures predictable state transitions and reduces the risk of unexpected side effects. **Do This:** * Use immutable data structures (e.g., Immutable.js, Immer) or libraries that enforce immutability. * Avoid mutating the state directly in reducers; create new state objects instead. * Freeze action payloads in development mode to detect accidental mutations. **Don't Do This:** * Directly modify the state object in reducers. * Pass mutable objects as action payloads. **Code Example:** """javascript // Using Immer to enforce immutability in reducers import { createSlice } from '@reduxjs/toolkit'; import { produce } from 'immer'; const initialState = { user: { id: '123', name: 'John Doe', profile: { age: 30, address : { street: "Main st" } }, }, }; export const userSlice = createSlice({ name: 'user', initialState, reducers: { updateName: (state, action) => { // mutate the draft state based on the action state.user.name = action.payload; }, updateAddress: (state, action ) => { state.user.profile.address = action.payload } } }) export const { updateName, updateAddress } = userSlice.actions export default userSlice.reducer """ ### 2.3. Thunk Security Considerations **Standard:** When using Redux Thunks, ensure that any API calls made within the thunk are secured and properly handle potential errors. **Why:** Thunks execute asynchronous logic and can introduce security vulnerabilities if not handled carefully. **Do This:** * Validate and sanitize any data sent to the API from the thunk. * Handle API errors gracefully and dispatch appropriate Redux actions to update the state. * Use HTTPS for all API calls. * Use "AbortController" for cancelling ongoing requests when needed to prevent unexpected state updates. **Don't Do This:** * Make API calls without proper error handling. * Expose sensitive data in API requests or responses. * Allow thunks to perform actions without proper authentication or authorization. **Code Example:** """javascript // Using Redux Thunk with secure API calls import { createAsyncThunk } from '@reduxjs/toolkit'; export const fetchData = createAsyncThunk( 'data/fetchData', async (arg, thunkAPI) => { try { const response = await fetch('/api/data', { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': "Bearer ${localStorage.getItem('token')}" // use local storage only as example. }, }); if (!response.ok) { // If the server responds with an error, include the error // message in the rejected promise return thunkAPI.rejectWithValue(await response.json()); } const data = await response.json(); return data; } catch (error) { // In case of network errors or exceptions, you can still reject // with a generic error message, or the specific error if appropriate return thunkAPI.rejectWithValue(error.message); } } ); // Handling the API response const dataSlice = createSlice({ name: 'data', initialState: { data: [], loading: 'idle', error: null }, reducers: {}, extraReducers: (builder) => { builder.addCase(fetchData.pending, (state) => { state.loading = 'loading'; }); builder.addCase(fetchData.fulfilled, (state, action) => { state.loading = 'idle'; state.data = action.payload; }); builder.addCase(fetchData.rejected, (state, action) => { state.loading = 'idle'; state.error = action.payload; // action.payload contains error from thunkAPI.rejectWithValue }); } }); """ ### 2.4. Serializability and Security **Standard:** Ensure that the Redux store contains only serializable data. Avoid storing non-serializable data such as functions, Promises, or class instances, especially those with complex internal states. **Why:** Storing non-serializable data can lead to unexpected behavior when using features like time-travel debugging, persist/rehydrate, or server-side rendering due to serialization and deserialization issues. Moreover, storing complex objects, particularly those from third-party libraries, increases the attack surface by introducing potentially exploitable code paths. **Do This:** * Only store plain JavaScript objects, arrays, primitive values (strings, numbers, booleans), or explicitly serializable data. * Use Redux middleware like "redux-persist" carefully, ensuring any transformations handle serialization and deserialization securely. * Be very wary of storing references to DOM nodes, sockets, or other external resources. **Don't Do This:** * Store functions, Promises, class instances, or other non-serializable data directly in the Redux store. * Store DOM nodes or other complex external resources. * Bypass the serializability checks in Redux DevTools without a very good reason and thorough security review. **Code Example (using "redux-persist" with transforms):** """javascript import { createSlice } from '@reduxjs/toolkit' import { persistStore, persistReducer, createTransform } from 'redux-persist' import storage from 'redux-persist/lib/storage' // defaults to localStorage for web // Example of serializing/deserializing a Date object: const dateTransform = createTransform( (inboundState, key) => { // Serialize if (inboundState && inboundState.lastLogin instanceof Date) { return { ...inboundState, lastLogin: inboundState.lastLogin.toISOString() }; } return inboundState; }, (outboundState, key) => { // Deserialize if (outboundState && typeof outboundState.lastLogin === 'string') { return { ...outboundState, lastLogin: new Date(outboundState.lastLogin) }; } return outboundState; }, { whitelist: ['user'] } // only apply to user state ); const userSlice = createSlice({ name: 'user', initialState: { name: 'John Doe', lastLogin: new Date(), // needs to be serialized // ...other user data }, reducers: { updateName(state, action) { state.name = action.payload }, }, }); const persistConfig = { key: 'root', storage, transforms: [dateTransform], } const persistedReducer = persistReducer(persistConfig, userSlice.reducer) import { configureStore } from '@reduxjs/toolkit' export const store = configureStore({ reducer: { user: persistedReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: ['persist/PERSIST'], // ignore persist actions ignoredPaths: ['user.lastLogin'], // or ignore specific pathes (less safe) warnAfter: 120, errorAfter: 240, }, }), }) export const persistor = persistStore(store) """ ## 3. Monitoring and Auditing ### 3.1. Logging Security-Related Events **Standard:** Log security-related events such as authentication attempts, authorization failures, and data modification attempts. **Why:** Provides visibility into potential security incidents and enables timely detection and response. **Do This:** * Use a centralized logging system to store and analyze security-related events. * Include relevant context information such as user IDs, timestamps, and action types in log entries. * Monitor logs regularly for suspicious activity. **Don't Do This:** * Log sensitive data such as passwords or API keys. * Store logs in a location accessible to unauthorized users. **Code Example:** """javascript // Redux middleware for logging security-related events const securityLogger = (store) => (next) => (action) => { const prevState = store.getState(); const result = next(action); const nextState = store.getState(); if (action.type === 'PERMISSION_DENIED') { console.warn("Security Alert: User "${prevState.auth.user?.id}" " + "attempted unauthorized action "${action.payload}""); // Send log to a server // sendLogToServer({ // user: prevState.auth.user?.id, // action: action.type, // payload: action.payload, // timestamp: new Date().toISOString() // }); } return result; }; // applying middleware (using Redux Toolkit configureStore) import { configureStore } from '@reduxjs/toolkit' const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(securityLogger) }) """ ### 3.2. Monitoring Redux Store **Standard:** Implement mechanisms to monitor the Redux store for unexpected state changes or data inconsistencies. **Why:** Helps detect and prevent security incidents in real-time. **Do This:** * Use Redux DevTools or custom middleware to track state changes. * Implement alerts for unexpected or suspicious state transitions. * Regularly audit the Redux store to ensure data integrity. **Don't Do This:** * Ignore or dismiss unexpected state changes. * Rely solely on manual inspections; automate monitoring processes. ## 4. Third-Party Libraries ### 4.1. Vetting Third-Party Libraries **Standard:** Thoroughly vet all third-party libraries used in Redux applications for security vulnerabilities and licensing issues. **Why:** Third-party libraries can introduce security vulnerabilities or licensing risks into your application. **Do This:** * Use tools like "npm audit" or "yarn audit" to identify known vulnerabilities. * Review the source code of third-party libraries to identify potential security flaws. * Ensure that the licenses of third-party libraries are compatible with your application's license. * Keep dependencies up to date. * Use tools like snyk.io to monitor projects **Don't Do This:** * Blindly trust third-party libraries without proper vetting. * Use outdated or unmaintained libraries. ### 4.2. Updating Dependencies **Standard:** Keep all Redux dependencies up to date to patch security vulnerabilities and benefit from the latest security enhancements. **Why:** Outdated dependencies are a common source of security vulnerabilities. **Do This:** * Use dependency management tools like "npm", "yarn", or "pnpm" to manage dependencies. * Regularly update dependencies using "npm update" or "yarn upgrade". * Automate dependency updates using tools like Dependabot. **Don't Do This:** * Ignore dependency updates or postpone them indefinitely. By following these security best practices, developers can build secure and robust Redux applications that are resilient to common vulnerabilities.