How can make user chosen category persist ?
store.js
import { combineReducers, configureStore } from "@reduxjs/toolkit"
import {
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import adminAdvertisementReducer from "./features/admin/advertisementSlice"
import usersReducer from "./features/users/usersSlice"
import cartReducer from "./features/cart/cartSlice"
import ordersReducer from "./features/orders/ordersSlice"
import productsReducer from "./features/products/productsSlice"
const persistConfig = {
key: "root",
version: 1,
storage
}
const **rootReducer** = combineReducers({
cart: cartReducer,
users: usersReducer,
// products: productsReducer
})
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const **store** = configureStore({
reducer: {
persistedReducer,
adminMainAdvertisement: adminAdvertisementReducer,
orders: ordersReducer,
products: productsReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from "react-redux";
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {store} from "./store";
import { PersistGate } from 'redux-persist/integration/react';
import persistStore from 'redux-persist/es/persistStore';
let persistor = persistStore(store)
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor} >
<App />
</PersistGate>
</Provider>
</React.StrictMode>
);
productsSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import apiAxios from "../../config/axiosConfig";
export const fetchAllProducts = createAsyncThunk("products/fetchAllProducts", async () => {
const response = await apiAxios.get("/products")
const products = {}
response.data.forEach(product => {
products[product.id] = product
})
return products
})
export const fetchProductsByCategory = createAsyncThunk("products/fetchProductsByCategory", async (category) => {
const response = await apiAxios.get(`/products/${category}`)
return response.data
})
export const productsSlice = createSlice({
name: "products",
initialState: {
allProducts: {},
productsInCategory: [],
categoryTheme: "",
**fetchAllProductsStatus: "idle",**
fetchProductsByCategoryStatus: "idle"
},
reducers: {
setCategoryTheme(state, action) {
state.categoryTheme = action.payload
}
},
extraReducers: (builder) => {
// Reduces for fetching products
builder
.addCase(fetchAllProducts.pending, (state, action) => {
state.fetchAllProductsStatus = "loading"
})
.addCase(fetchAllProducts.fulfilled, (state, action) => {
state.fetchAllProductsStatus = "succeeded"
state.allProducts = action.payload
})
.addCase(fetchAllProducts.rejected, (state, action) => {
state.fetchAllProductsStatus = "failed"
})
// fetchProductsByCategory
.addCase(fetchProductsByCategory.pending, (state, action) => {
state.fetchProductsByCategoryStatus = "loading"
})
.addCase(fetchProductsByCategory.fulfilled, (state, action) => {
state.fetchProductsByCategoryStatus = "succeeded"
state.productsInCategory = action.payload
})
.addCase(fetchProductsByCategory.rejected, (state, action) => {
state.fetchProductsByCategoryStatus = "failed"
})
}
})
export const { setCategoryTheme } = productsSlice.actions
export const selectAllProducts = state => state.products.allProducts
export const selectProductsInCategory = state => state.products.productsInCategory
export const selectProductById = (state, productId) => state.products.allProducts[productId]
export const selectCategoryTheme = state => state.products.categoryTheme
**export const selectFetchAllProductsStatus = state => state.products.fetchAllProductsStatus**
export const selectFetchProductsByCategoryStatus = state => state.products.fetchProductsByCategoryStatus
export default productsSlice.reducer
dropDownTab.js
import { Link, useHistory } from "react-router-dom";
import "../nav/nav.css"
import { useDispatch, useSelector } from "react-redux";
import { fetchProductsByCategory, selectFetchProductsByCategoryStatus, setCategoryTheme } from "../../features/products/productsSlice";
import { useEffect, useState } from "react";
const DropdownTab = ({name, items}) => {
const dispatch = useDispatch()
const history = useHistory()
const fetchProductsByCategoryStatus = useSelector(selectFetchProductsByCategoryStatus)
const [category, setCategory] = useState("")
// Sorting items a-z
const ascItems = items.sort((a, b) => {
return a.localeCompare(b)
})
useEffect(() => {
if (fetchProductsByCategoryStatus === "succeeded") {
history.push(`/products/category/${category}`)
}
}, [history, fetchProductsByCategoryStatus, category])
const handleClick = (item) => {
setCategory(item)
dispatch(setCategoryTheme(item))
dispatch(fetchProductsByCategory(item))
}
return (
<div class="dropdown" >
<span class="dropdown-toggle navItemMd" data-bs-toggle="dropdown" aria-expanded="false">
{name}
</span>
** <ul className="dropdown-menu navItemMd categoryTab text-decoration-none" autoClose="false">
{
ascItems
? ascItems.map((item) => {
return (
<li key={item} className="active">
<Link to="" className="dropdown-item" onClick={() => handleClick(item)}>{item}</Link>
</li>
)
})
: <span>Category is empty</span>
}
</ul>**
</div>
);
}
export default DropdownTab;
Category.js
import "./category.css";
import { useDispatch, useSelector } from "react-redux";
import {
fetchProductsByCategory, selectCategoryTheme, selectFetchProductsByCategoryStatus, selectProductsInCategory
} from "../../features/products/productsSlice";
import { Link, useParams } from "react-router-dom";
import { useEffect, useState } from "react";
const ProductCategory = () => {
const dispatch = useDispatch()
// const categoryUrl = useParams().category
const fetchProductsByCategoryStatus = useSelector(selectFetchProductsByCategoryStatus)
const productsInCategory = useSelector(selectProductsInCategory)
const categoryTheme = useSelector(selectCategoryTheme)
const [category, setCategory] = useState("")
console.log("category.js ", category, categoryTheme, fetchProductsByCategoryStatus, productsInCategory)
useEffect(() => {
if (fetchProductsByCategoryStatus === "succeeded") {
setCategory(productsInCategory[0].category)
} else if (fetchProductsByCategoryStatus === "idle" && categoryTheme) {
console.log("2")
dispatch(fetchProductsByCategory(categoryTheme))
} else {
console.log("test")
}
}, [fetchProductsByCategoryStatus, categoryTheme, category])
return (
<div className="container p2" style={{minHeight:"100vh"}}>
<nav aria-label="breadcrumb">
<ol class="breadcrumb" style={{fontFamily:"serif", fontSize:"0.6rem", marginTop:"0.4rem", marginBottom:"0.4rem"}}>
<li class="breadcrumb-item"><Link to ="/">Home</Link></li>
<li class="breadcrumb-item active" aria-current="page">Category</li>
</ol>
</nav>
<div style={{fontFamily:"serif"}}>{category}</div>
**<div className="d-md-flex justify-content-center align-items-center">
{
productsInCategory.length <= 0
? <p>This category is currently empty.</p>
: productsInCategory.map(item => {
return (
<div key={item.id} className="card shadow m-md-2 my-2 col-12 col-md-4">
<img src={item.img_url} class="card-img-top" alt="item in category" />
<div class="card-body setCardBody">
<p class="card-title"><b>Name: </b><span>{item.title}</span></p>
<div>
<span><b>Price: $ </b>{item.price}</span>
<span> | <b>Status: </b>{item.status}</span>
</div>
<p class="card-text"><b>Description: </b><span>{item.description}</span></p>
<button class="btn btn-primary setCardBody">Add to cart</button>
</div>
</div>
)})
}
</div> **
</div>
);
}
export default ProductCategory;
in store.js, I have tried to put productsReducer in rootReducer and not in store. But it results in error: undefined fetchAllProductsStatus. If I commented out the productsReducer in rootReducer and put it in store, user can choose the category, but the error in the title emerges.
info: user chooses category in dropDownTab.js and the page goes to category.js. If category page is refreshed, user input is deleted automatically (This is the problem).
I suspect the problem here is that when you try to persist the products
state in persistedReducer
and then use persistedReducer
as a root-level reducer it updates the path to the products state in the state tree, i.e. the structure of the state formed by combining the reducers. The access path then becomes state.persistedReducer.products
instead of just state.products
. All the defined products state slice selector functions have the incorrect path.
You have a couple options available to you for persisting the root state sections.
Individual persisted root-level reducers:
const persistCartConfig = {
key: "cart",
version: 1,
storage,
};
const persistedCartReducer = persistReducer(
persistCartConfig,
cartReducer
);
const persistProductsConfig = {
key: "products",
version: 1,
storage,
};
const persistedProductsReducer = persistReducer(
persistProductsConfig,
productsReducer
);
const persistUserConfig = {
key: "user",
version: 1,
storage,
};
const persistedUserReducer = persistReducer(
persistUserConfig,
usersReducer
);
const rootReducer = combineReducers({
// Persisted state
cart: persistedCartReducer,
products: persistedProductsReducer,
users: persistedUserReducer,
// Regular un-persisted state
adminMainAdvertisement: adminAdvertisementReducer,
orders: ordersReducer,
});
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
});
Single persisted root reducer with whitelist/blacklist keys to only persist specific areas:
const persistConfig = {
key: "root",
version: 1,
storage,
whitelist: ["cart", "user", "products"],
};
const rootReducer = combineReducers({
// Whitelisted persisted state
cart: cartReducer,
products: productsReducer,
users: usersReducer,
// Regular un-persisted state
adminMainAdvertisement: adminAdvertisementReducer,
orders: ordersReducer,
});
const persistedRootReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
});