Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/skyybbanerjee/advance_redux
Some advance concepts about Redux, RTK, Async. data and side-effects handling. π¨π»βπ»β³
https://github.com/skyybbanerjee/advance_redux
Last synced: 3 days ago
JSON representation
Some advance concepts about Redux, RTK, Async. data and side-effects handling. π¨π»βπ»β³
- Host: GitHub
- URL: https://github.com/skyybbanerjee/advance_redux
- Owner: skyybbanerjee
- Created: 2024-12-17T19:23:00.000Z (22 days ago)
- Default Branch: main
- Last Pushed: 2024-12-17T19:34:46.000Z (22 days ago)
- Last Synced: 2024-12-27T19:29:43.183Z (12 days ago)
- Language: JavaScript
- Size: 207 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
In **Redux Toolkit (RTK)**, **action creators** are functions that automatically generate action objects for you based on the reducers you define. They simplify dispatching actions by removing the need to manually create action objects with `type` and `payload`.
### Basics:
When you use `createSlice` in RTK, it automatically generates **action creators** for each reducer function inside the slice. These action creators can be called directly in your components or other parts of the app.---
### **What is an Action Creator?**
An **action creator** is a function that returns an action object. For example:
```js
const addTodo = (payload) => {
return {
type: "ADD_TODO",
payload,
};
};
```In RTK, you don't need to manually write such functions because **`createSlice` does it for you**.
---
### Example of Action Creators in RTK:
#### **Step 1: Create a Slice**
```js
import { createSlice } from "@reduxjs/toolkit";const todoSlice = createSlice({
name: "todo",
initialState: { items: [] },
reducers: {
addTodo(state, action) {
state.items.push({ id: Date.now(), text: action.payload });
},
removeTodo(state, action) {
state.items = state.items.filter((item) => item.id !== action.payload);
},
},
});export const todoActions = todoSlice.actions; // Generated action creators
export default todoSlice.reducer;
```Here:
- RTK automatically creates **action creators**:
- `todoActions.addTodo`
- `todoActions.removeTodo`---
#### **Step 2: Use Action Creators in a Component**
You can call these action creators directly and dispatch them using `useDispatch`:
```js
import { useDispatch } from "react-redux";
import { todoActions } from "./todoSlice";function TodoApp() {
const dispatch = useDispatch();function addHandler() {
dispatch(todoActions.addTodo("Learn Redux Toolkit"));
}function removeHandler(id) {
dispatch(todoActions.removeTodo(id));
}return (
Add Todo
);
}
```Here:
- `todoActions.addTodo("Learn Redux Toolkit")` automatically generates the following action object:
```js
{
type: "todo/addTodo",
payload: "Learn Redux Toolkit"
}
```- Similarly, `todoActions.removeTodo(id)` generates an action with the appropriate type and payload.
---
### Key Benefits of Action Creators in RTK:
1. **Automatic Generation**: You don't need to write action creators manually. RTK handles it.
2. **Consistency**: Actions follow a consistent structure.
3. **Readability**: Your code becomes cleaner and easier to read.
4. **Error Reduction**: Avoids typos in `type` strings by using generated functions.---
### Thunks as Action Creators (for Async Logic)
In RTK, you can also write **thunks** as action creators for async tasks:
```js
import { createAsyncThunk } from "@reduxjs/toolkit";export const fetchTodos = createAsyncThunk("todo/fetchTodos", async () => {
const response = await fetch("https://api.example.com/todos");
const data = await response.json();
return data; // This becomes the payload
});
```
Here, `fetchTodos` is an action creator that can be dispatched.---
### Conclusion:
In RTK:
- **Action Creators** are automatically generated when you use `createSlice`.
- They save time, reduce errors, and simplify Redux code.
![image](https://github.com/user-attachments/assets/3165dc8f-4ee8-42dc-8d3e-08a47e698770)
### Without thunks:
```js
//src>fetaures>cartSlice.jsimport { createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: {
items: [],
totalQuantity: 0,
},
reducers: {
addItem(state, action) {
const item = action.payload;
const existingItem = state.items.find((i) => i.id === item.id); // Check existing item
state.totalQuantity++; // Increment totalQuantity
if (!existingItem) {
// Add a new item if not already present
state.items.push({
id: item.id,
price: item.price,
quantity: 1,
totalPrice: item.price,
name: item.title,
});
} else {
// Increase quantity and update total price
existingItem.quantity++;
existingItem.totalPrice += item.price;
}
},removeItem(state, action) {
const itemId = action.payload;
const existingItem = state.items.find((i) => i.id === itemId);
if (!existingItem) return; // Safety check
state.totalQuantity--;
if (existingItem.quantity === 1) {
state.items = state.items.filter((i) => i.id !== itemId);
} else {
existingItem.quantity--;
existingItem.totalPrice -= existingItem.price;
}
},
},
});export const cartActions = cartSlice.actions;
export default cartSlice;
```
```js
//src>App.jsimport { useDispatch, useSelector } from "react-redux";
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
import { useEffect } from "react";
import { uiActions } from "./features/uiSlice";
import Notification from "./components/UI/Notification";let isInitial = true;
function App() {
const dispatch = useDispatch();
const showCart = useSelector((store) => store.ui.cartIsVisible);
const cart = useSelector((store) => store.cart);
console.log("cartIsVisible:", showCart);
const notification = useSelector((store) => store.ui.notification); // Corrected here to 'notification'useEffect(() => {
const sendCartData = async () => {
dispatch(
uiActions.showNotification({
status: "pending",
title: "Sending request.. β³",
message: "Sending cart data! πβ",
})
);const response = await fetch(
"https://redux-academind-default-rtdb.asia-southeast1.firebasedatabase.app/cart.json",
{
method: "PUT",
body: JSON.stringify(cart),
}
);if (!response.ok) {
throw new Error("Sending cart data failed β οΈ");
}// DISPATCH success notificationn
dispatch(
uiActions.showNotification({
status: "success",
title: "Success! π",
message: "Cart data sent successfully β π",
})
);
};if (isInitial) {
isInitial = false;
return;
}sendCartData().catch((error) => {
// DISPATCH error notification
dispatch(
uiActions.showNotification({
status: "error",
title: "Error!β οΈ",
message: "Sending cart data failedββ οΈ",
})
);
});
}, [cart, dispatch]);return (
<>
{/* Render Notification */}
{notification && (
)}
{showCart && }
>
);
}export default App;
```
In **Redux Toolkit**, handling asynchronous code (like API calls or side effects) is typically done using the following approaches:---
### 1. **Redux Toolkit's `createAsyncThunk`**
`createAsyncThunk` is the most common and recommended way to handle async logic. It simplifies asynchronous actions by allowing you to write **async/await** logic without dealing with boilerplate.#### **Example**: Fetching data from an API
```javascript
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";// Async action creator
export const fetchProducts = createAsyncThunk(
"products/fetchProducts", // Action type
async (_, { rejectWithValue }) => {
try {
const response = await fetch("https://fakestoreapi.com/products");
if (!response.ok) {
throw new Error("Failed to fetch products");
}
const data = await response.json();
return data; // Automatically dispatched as `fulfilled` action
} catch (error) {
return rejectWithValue(error.message); // Handle errors
}
}
);const productsSlice = createSlice({
name: "products",
initialState: {
products: [],
loading: false,
error: null,
},
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.loading = false;
state.products = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || "Something went wrong";
});
},
});export default productsSlice.reducer;
```#### **Usage in Components**:
```javascript
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchProducts } from "./productsSlice";const ProductList = () => {
const dispatch = useDispatch();
const { products, loading, error } = useSelector((state) => state.products);useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);if (loading) return
Loading...
;
if (error) returnError: {error}
;return (
- {product.title}
{products.map((product) => (
))}
);
};
export default ProductList;
```
---
### 2. **Middleware like Redux Thunk**
Under the hood, `createAsyncThunk` uses **Redux Thunk** middleware. If you prefer manually handling async logic with Thunks, you can write them explicitly.
#### **Example**: Manual Redux Thunk
```javascript
// Actions
export const fetchProducts = () => {
return async (dispatch) => {
dispatch({ type: "products/fetchPending" });
try {
const response = await fetch("https://fakestoreapi.com/products");
const data = await response.json();
dispatch({ type: "products/fetchFulfilled", payload: data });
} catch (error) {
dispatch({ type: "products/fetchRejected", payload: error.message });
}
};
};
```
---
### 3. **RTK Query** (Advanced Option)
If you're building APIs and require an **automatic caching and data fetching solution**, Redux Toolkit provides **RTK Query**. It simplifies data fetching and caching.
#### **Setup Example**:
```javascript
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const productsApi = createApi({
reducerPath: "productsApi",
baseQuery: fetchBaseQuery({ baseUrl: "https://fakestoreapi.com" }),
endpoints: (builder) => ({
getProducts: builder.query({
query: () => "/products",
}),
}),
});
export const { useGetProductsQuery } = productsApi;
```
#### **Usage in Components**:
```javascript
import { useGetProductsQuery } from "./productsApi";
const ProductList = () => {
const { data: products, error, isLoading } = useGetProductsQuery();
if (isLoading) return
Loading...
;if (error) return
Error: {error.message}
; return (
- {product.title}
{products.map((product) => (
))}
);
};
export default ProductList;
```
---
### Summary: When to Use What?
1. **`createAsyncThunk`**: Ideal for most async tasks like API requests with error handling.
2. **Custom Thunks**: Use if you want manual control or more flexibility.
3. **RTK Query**: Best for advanced use cases where data fetching, caching, and re-fetching are required.
For most projects, **`createAsyncThunk`** or **RTK Query** is sufficient and recommended.
**Thunks** are a concept in programming that allow you to handle **asynchronous operations** (like API calls, timers, or side effects) in a **synchronous flow**. In the context of **Redux**, a "thunk" is a **middleware function** that helps you handle async logic in your actions and provides more control over dispatching actions.
---
### **What Problem Do Thunks Solve?**
Redux's basic flow is **synchronous**, which means actions are dispatched, and reducers update the state immediately. However, many real-world applications require **asynchronous operations**, like:
- Fetching data from an API
- Posting data to a server
- Performing timeouts or delays
Redux itself does not support async logic out of the box. Thunks help bridge this gap.
---
### **What is a Thunk?**
A **Thunk** is a function that wraps an **expression** (or logic) and delays its execution. In Redux, thunks are functions that **return another function** instead of an action object.
- The returned function gets access to two important arguments:
- **`dispatch`**: Allows you to dispatch actions (e.g., success or error).
- **`getState`**: Lets you access the Redux store's current state.
---
### **How Does Redux Thunk Work?**
When you use the **`redux-thunk`** middleware, it intercepts actions you dispatch. If the dispatched action is a **function** (instead of a plain object), the middleware executes that function. This allows you to write **async logic** inside your action creators.
---
### **Example of a Basic Redux Thunk**
#### **1. Install Redux Thunk Middleware**
```bash
npm install redux-thunk
```
#### **2. Configure Middleware in Your Store**
```javascript
import { configureStore } from "@reduxjs/toolkit";
import thunk from "redux-thunk";
import cartReducer from "./cartSlice";
const store = configureStore({
reducer: { cart: cartReducer },
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(thunk),
});
export default store;
```
#### **3. Write Thunk Logic**
Here's a simple example of an async thunk to fetch data from an API.
```javascript
// Action Creator with Thunk
export const fetchProducts = () => {
// This function is intercepted by redux-thunk
return async (dispatch, getState) => {
dispatch({ type: "products/fetchPending" }); // Set loading state
try {
const response = await fetch("https://fakestoreapi.com/products");
if (!response.ok) {
throw new Error("Failed to fetch products!");
}
const data = await response.json();
// Dispatch success action
dispatch({ type: "products/fetchFulfilled", payload: data });
} catch (error) {
// Dispatch error action
dispatch({ type: "products/fetchRejected", payload: error.message });
}
};
};
```
In this example:
1. The **`fetchProducts`** action creator doesn't immediately return an action object.
2. It returns a function that performs an API call using **`async/await`**.
3. The **dispatch** function is used inside the async function to trigger "pending," "success," and "error" actions.
---
### **4. Update Reducer to Handle Actions**
```javascript
const initialState = {
products: [],
loading: false,
error: null,
};
const productsReducer = (state = initialState, action) => {
switch (action.type) {
case "products/fetchPending":
return { ...state, loading: true, error: null };
case "products/fetchFulfilled":
return { ...state, loading: false, products: action.payload };
case "products/fetchRejected":
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};
export default productsReducer;
```
---
### **5. Use Thunk in a React Component**
```javascript
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchProducts } from "./productsActions";
const Products = () => {
const dispatch = useDispatch();
const { products, loading, error } = useSelector((state) => state.products);
useEffect(() => {
dispatch(fetchProducts()); // Dispatch the thunk
}, [dispatch]);
if (loading) return
Loading...
;if (error) return
Error: {error}
; return (
- {product.title}
{products.map((product) => (
))}
);
};
export default Products;
```
---
### **Why Thunks Are Useful**
1. **Handles Asynchronous Code**: Thunks allow you to perform async operations (e.g., API calls, timers) in Redux.
2. **Sequential Actions**: You can dispatch multiple actions in sequence (e.g., pending, success, and error states).
3. **Access to State**: The **`getState`** argument lets you read the current state inside your thunk logic.
4. **Avoid Boilerplate**: Thunks reduce boilerplate compared to writing everything manually.
---
### **Thunk vs Regular Action Creator**
| **Regular Action Creator** | **Thunk Action Creator** |
|-------------------------------------------|---------------------------------------------|
| Returns a plain action object | Returns a function instead of an object |
| Synchronous only | Can handle asynchronous logic |
| No access to `dispatch` or `getState` | Has access to `dispatch` and `getState` |
| Example: `{ type: 'ACTION_TYPE', payload }`| Example: `return (dispatch) => { ... }` |
---
### **Modern Alternative to Thunks**
- While **thunks** are widely used, Redux Toolkit introduced **`createAsyncThunk`** (which internally uses thunks) to simplify async code even further. It reduces boilerplate and handles common async patterns automatically.
For example:
```javascript
export const fetchProducts = createAsyncThunk("products/fetchProducts", async () => {
const response = await fetch("https://fakestoreapi.com/products");
return await response.json();
});
```
---
### **Conclusion**
Thunks are a powerful way to handle asynchronous code in Redux. They are middleware functions that allow you to perform async operations (like API calls) and dispatch actions based on the result (e.g., "pending," "success," "error"). While you can write thunks manually, tools like `createAsyncThunk` make it even easier to manage async logic.
Using `useEffect()` for async code like API calls is not *wrong*, but it has limitations compared to handling side effects using Redux Thunks or similar middleware. While both approaches can work, using `useEffect()` has several downsides in the context of **global state management** with Redux. Letβs break this down:
---
### **1. Separation of Concerns**
**Problem with `useEffect()`**:
- `useEffect` is designed for **local side effects** in a component, not global state management.
- If you fetch data or handle async logic directly in `useEffect()`, your **component logic becomes tightly coupled with your async logic**, making the code harder to test and maintain.
**With Thunks**:
- Thunks separate the **business logic** (API calls, async tasks) from the **UI components**.
- This makes your components cleaner, reusable, and easier to test, since the logic resides in Redux actions or thunks.
**Example of tightly coupled useEffect logic**:
```jsx
useEffect(() => {
const fetchData = async () => {
const response = await fetch('API_URL');
const data = await response.json();
setState(data); // Local or global state
};
fetchData();
}, []);
```
With thunks, the async code would live outside the component:
```jsx
useEffect(() => {
dispatch(fetchDataThunk()); // Async logic is abstracted away
}, [dispatch]);
```
---
### **2. Code Duplication**
**Problem with `useEffect()`**:
- If multiple components need the same async data (like fetching user data), you must duplicate the `useEffect()` logic across components.
**With Thunks**:
- A single thunk can fetch the data and store it in the Redux store. Multiple components can access the **same global state** using `useSelector`.
**Example**:
```jsx
// Repeated in multiple components
useEffect(() => {
const fetchUser = async () => {
const response = await fetch('/api/user');
const userData = await response.json();
setUser(userData);
};
fetchUser();
}, []);
```
With Redux Thunks:
- You fetch the user data **once** in a thunk and share it globally:
```javascript
export const fetchUser = () => async (dispatch) => {
const response = await fetch('/api/user');
const data = await response.json();
dispatch(userActions.setUser(data));
};
```
Components just use:
```jsx
const user = useSelector((state) => state.user);
```
---
### **3. Limited Control Over Dispatching Actions**
**Problem with `useEffect()`**:
- `useEffect` alone cannot handle intermediate states like **loading, success, and error** easily without adding extra flags (`isLoading`, `isError`, etc.) locally.
**With Thunks**:
- You can dispatch multiple actions to handle loading, success, and error states.
**Example**:
```javascript
export const fetchData = () => async (dispatch) => {
dispatch({ type: 'fetch/pending' }); // Set loading state
try {
const response = await fetch('/api/data');
const data = await response.json();
dispatch({ type: 'fetch/success', payload: data });
} catch (error) {
dispatch({ type: 'fetch/error', payload: error.message });
}
};
```
Components handle the global state:
```jsx
const { data, isLoading, error } = useSelector((state) => state.fetch);
```
With `useEffect`, this becomes repetitive and harder to maintain.
---
### **4. Scalability Issues**
**Problem with `useEffect()`**:
- As your app grows, if every async operation is handled in `useEffect()`, it becomes unmanageable. Every component deals with its own logic for API calls, state management, and error handling.
**With Thunks**:
- Thunks centralize async logic in Redux, making it easier to scale. Adding or modifying async logic doesn't affect your components.
---
### **5. Testing**
**Problem with `useEffect()`**:
- Testing components with `useEffect()` requires mocking the entire network request, which can be cumbersome.
**With Thunks**:
- You can test thunks independently of your components using tools like **Jest** and **Mock Service Worker**.
Example of testing a thunk:
```javascript
it('should dispatch success action after API call', async () => {
const dispatch = jest.fn();
const mockResponse = { data: [1, 2, 3] };
global.fetch = jest.fn(() =>
Promise.resolve({ json: () => Promise.resolve(mockResponse) })
);
await fetchData()(dispatch);
expect(dispatch).toHaveBeenCalledWith({ type: 'fetch/pending' });
expect(dispatch).toHaveBeenCalledWith({ type: 'fetch/success', payload: mockResponse });
});
```
---
### **Summary: When to Use Thunks Instead of `useEffect()`**
| **Criteria** | **useEffect** | **Redux Thunks** |
|--------------------------------|--------------------------------------------------|----------------------------------------------|
| **Scope** | Local to the component | Global async logic (reusable across app) |
| **Code Reusability** | Requires duplicating logic in multiple places | Centralized logic, reusable everywhere |
| **Error Handling** | Needs manual flags for loading/error states | Handles multiple states with dispatch actions|
| **State Management** | Local or tightly coupled to the component | Global state with Redux |
| **Testing** | Harder to test, tightly coupled to UI | Thunks are easier to test independently |
| **Scalability** | Becomes unmanageable in large apps | Scalable and maintainable |
---
### **When Is `useEffect()` Acceptable?**
- Small components with **local state**.
- Simple side effects that donβt need to be reused globally.
- For small projects where Redux is unnecessary.
---
### **Conclusion**
While `useEffect()` works for basic side effects, using **Redux Thunks** is the preferred approach for handling **asynchronous operations** in applications with global state. Thunks offer cleaner separation of concerns, scalability, and better reusability, while keeping the components simple and focused on UI rendering.