Im building a shopping cart with React and Redux and failing to understand the best practice flow.
My Cart ACTIONS:
export const addToCart = (product) => (dispatch, getState) => {
let { products: cartProducts } = getState().cart;
let { newCartProducts, totalPrice } = handleAddToCart(cartProducts, product);
dispatch(
add({
products: newCartProducts,
total: totalPrice,
})
);
};
Mock-Server handlers: (All the logic of updating the product is going here => my main question is if this is makes any sense.
export function handleAddToCart(cartProducts, currentProduct) {
let idx = cartProducts.findIndex((p) => p.id === currentProduct.id);
let productInCart = cartProducts[idx];
if (productInCart) {
let updatedProduct = {
...currentProduct,
quantity: productInCart.quantity + 1,
price:
productInCart.price +
applySale({
...currentProduct,
quantity: productInCart.quantity + 1,
currentTotal: productInCart.price,
}),
};
cartProducts.splice(idx, 1, updatedProduct);
} else cartProducts.push({ ...currentProduct, quantity: 1 });
let totalPrice = cartProducts.reduce((acc, val) => (acc += val.price), 0);
return { newCartProducts: cartProducts, totalPrice };
}
Cart reducer:
};
export default (state = DEFAULT_STATE, action) => {
switch (action.type) {
case "ADD_TO_CART":
return {
products: [...action.payload.products],
total: action.payload.total,
};
default:
return DEFAULT_STATE;
}
};
As you see from the code, I kept action and reducer logic to minimum and let the handler manipulate data. only after the data is manipulated, I insert it to the state.
After giving it thought, the reducer ADD_TO_CART is only symbolic because it gets an array and not an item so it can be actually a multi purpose reducer which is i think not so good.
Would be great to hear more opinions.
We specifically recommend putting as much logic as possible into reducers, and treating actions as "events" that describe "what happened" with the minimal amount of data inside.
Also, note that you should be using our official Redux Toolkit package, which will drastically simplify your Redux logic.
Related
can anyone please explain me this code? I am not able to understand as in what's happening here.
const cartReducer = (state, action) => {
if (action.type === "ADD") {
const updatedTotalAmount =
state.totalAmount + action.item.price * action.item.amount;
const existingCartItemIndex = state.items.findIndex(
(item) => item.id === action.item.id
);
const existingCartItem = state.items[existingCartItemIndex];
let updatedItems;
if (existingCartItem) {
const updatedItem = {
...existingCartItem,
amount: existingCartItem.amount + action.item.amount,
};
updatedItems = [...state.items];
updatedItems[existingCartItemIndex] = updatedItem;
} else {
updatedItems = state.items.concat(action.item);
}
return {
items: updatedItems,
totalAmount: updatedTotalAmount,
};
}
return defaultCartState;
};
That is a redux reducer. Please read this tutorial to get familiar with the concepts of it:
https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers
Reducers were popularized by Redux but are not a concept inherent to Redux in the sense that you can write a reducer without any import from Redux. A reducer is a concept for a particular kind of function i.e.:
a function that receives the current state and an action object, decides how to update the state if necessary, and returns the new state: (state, action) => newState. "Reducer" functions get their name because they're similar to the kind of callback function you pass to the Array.reduce() method.
Source: Redux docs
React now comes with a useReducer hook built-in. See Hooks API Reference.
I have added some comments to your code I hope this makes the code a bit more understandable.
const cartReducer = (state, action) => {
// Adding an Item to Cart
if (action.type === "ADD") {
// Calculated Cart Total: existing Total + (new Item Price * new item Quantity)
const updatedTotalAmount = state.totalAmount + action.item.price * action.item.amount;
/*
* Finding Items Index in the Cart Array using the Item ID.
* Index will be Returned only if Item with same od already exist otherwise -1
*/
const existingCartItemIndex = state.items.findIndex((item) => item.id === action.item.id);
/*
* Getting the CartItem Based on the Index.
* If the value is -1 i.e., item already doesn't exist, then this code will return undefined
*/
const existingCartItem = state.items[existingCartItemIndex];
let updatedItems;
// existingCartItem will have an Object(which evaluates to true) only if Item already existed in Cart
if (existingCartItem) {
// Creating updatedItem by spreading the existingItems data + updating amount/Quantity to: existing Quantity + new Quantity
const updatedItem = {
...existingCartItem,
amount: existingCartItem.amount + action.item.amount,
};
// Making a Copy of Items Array & Replacing Existing Item with new/updated entry
updatedItems = [...state.items];
updatedItems[existingCartItemIndex] = updatedItem;
} else {
// If the Item doesn't already exist in Cart then we Just add that New Item to the Cart
updatedItems = state.items.concat(action.item);
}
// Return the State with Updated Items List & total Amount
return {
items: updatedItems,
totalAmount: updatedTotalAmount,
};
}
// Default State Return
return defaultCartState;
};
i got two values i.e.company and id from navigation.
let id = props.route.params.oved;
console.log("id-->",id);
let company = props.route.params.company;
console.log("company--->",company);
i got two values as a integer like this:--
id-->1
comapny-->465
Description of the image:---
if i am giving input 1 in that textInput and click on the card(lets say first card i.e.465 then i am getting those two values in navigation as in interger that i have mention above.so each time i am getting updated values.
i am getting updated values from navigation.
so i want to store those values in redux.
action.js:--
import { CHANGE_SELECTED_COMPANY } from "./action-constants";
export const changeCompany = (updatedCompany, updatedId) => {
return {
type: CHANGE_SELECTED_COMPANY,
updatedCompany,
updatedId,
};
};
reducer.js:--
import { CHANGE_SELECTED_COMPANY } from "../actions/action-constants";
const initialState = {
company: "",
id: "",
};
const changeCompanyReducer = (state = initialState, action) => {
switch (action.type) {
case CHANGE_SELECTED_COMPANY:
return {
company: {
company: action.updatedCompany,
id: action.updatedId,
},
};
}
return state;
};
export default changeCompanyReducer;
congigure-store.js:--
import changeCompanyReducer from "./reducers/change-company-reducer";
const rootReducer = combineReducers({changeCompanyReducer});
How can i store the update values getting from navigation in Redux?
could you please write code for redux??
in the component create a function that updates the values
const updateReducer = () => {
dispatch(changeCompany(props.route.params.oved, props.route.params.company))
}
then call the function in react navigation lifecycle event
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
updateReducer()
});
return unsubscribe;
}, [navigation])
its possible that a better solution would be to update the reducer before the navigation happens and not pass the data in the params but rather pull it from redux but this is the answer to the question as asked
I have the following code (I deleted most of it to make it easier to understand - but everything works):
"role" reducer:
// some async thunks
const rolesSlice = createSlice({
name: "role",
initialState,
reducers: { // some reducers here },
extraReducers: {
// a bunch of extraReducers
[deleteRole.fulfilled]: (state, { payload }) => {
state.roles = state.roles.filter((role) => role._id !== payload.id);
state.loading = false;
state.hasErrors = false;
},
},
});
export const rolesSelector = (state) => state.roles;
export default rolesSlice.reducer;
"scene" reducer:
// some async thunks
const scenesSlice = createSlice({
name: "scene",
initialState,
reducers: {},
extraReducers: {
[fetchScenes.fulfilled]: (state, { payload }) => {
state.scenes = payload.map((scene) => scene);
state.loading = false;
state.scenesFetched = true;
}
}
export const scenesSelector = (state) => state.scenes;
export default scenesSlice.reducer;
a component with a button and a handleDelete function:
// a react functional component
function handleDelete(role) {
// some confirmation code
dispatch(deleteRole(role._id));
}
My scene model (and store state) looks like this:
[
{
number: 1,
roles: [role1, role2]
},
{
number: 2,
roles: [role3, role5]
}
]
What I am trying to achieve is, when a role gets deleted, the state.scenes gets updated (map over the scenes and filter out every occurrence of the deleted role).
So my question is, how can I update the state without calling two different actions from my component (which is the recommended way for that?)
Thanks in advance!
You can use the extraReducers property of createSlice to respond to actions which are defined in another slice, or which are defined outside of the slice as they are here.
You want to iterate over every scene and remove the deleted role from the roles array. If you simply replace every single array with its filtered version that's the easiest to write. But it will cause unnecessary re-renders because some of the arrays that you are replacing are unchanged. Instead we can use .findIndex() and .splice(), similar to this example.
extraReducers: {
[fetchScenes.fulfilled]: (state, { payload }) => { ... }
[deleteRole.fulfilled]: (state, { payload }) => {
state.scenes.forEach( scene => {
// find the index of the deleted role id in an array of ids
const i = scene.roles.findIndex( id => id === payload.id );
// if the array contains the deleted role
if ( i !== -1 ) {
// remove one element starting from that position
scene.roles.splice( i, 1 )
}
})
}
}
I need advice on where to perform data filtering to achieve best performance. Let's say I receive a big array of products from one endpoint of a remote API and product categories from another endpoint. I store them in Redux state and also persist to Realm database so that they are available for offline usage.
In my app, I have a Stack.Navigator that contains 2 screens: ProductCategories and ProductsList. When you press on a category it brings you to the screen with products that fall under that category. Currently, I perform the data filtering right inside my component, from my understanding it fires off every time the component is rendered and I suspect this approach slows down the app.
So I was wondering if there is a better way of doing that? Maybe filter the data for each category in advance when the app is loading?
My code looks as follows:
const ProductCategories = (props) => {
const isFocused = useIsFocused();
useEffect(() => {
if (isFocused) {
setItems(props.productCategories);
}
}, [isFocused]);
return (
...
);
};
const mapStateToProps = (state) => ({
productCategories: state.catalog.productCategories,
});
const ProductsList = (props) => {
const isFocused = useIsFocused();
const productsFilteredByCategory = props.products.filter((product) => {
return product.category === id;
});
useEffect(() => {
if (isFocused) {
setItems(productsFilteredByCategory);
}
}, [isFocused]);
return (
...
)
const mapStateToProps = (state) => ({
products: state.catalog.products,
});
You have to normalize (you can see main principles here) data in redux, to the next view:
// redux store
{
categories: {
1: { // id of category
id: 1,
title: 'some',
products: [1, 2, 3] // ids of products
},
...
},
products: {
1: { // id of product
id: 1,
title: 'some product',
},
...
}
}
Then you can create few selectors which will be even without memoization work much faster then filter, because time of taking data from object by property is constant
const getCategory = (state, categoryId) => state.categories[categoryId]
const getProduct = (state, productId) => state.products[productId]
const getCategoryProducts = (state, categoryId) => {
const category = getCategory(state, categoryId);
if (!category) return [];
return category.products.map((productId) => getProduct(state, productId))
}
I'm kind of new to React.js & Redux, so I have encountered a problem with Reducers.
I am creating a site that have a main "Articles" page, "Question & Answers" page, I created for each one a separate Reducer that both work just fine.
The problem is in "Main Page" which contains a lot of small different pieces of information, and I don't want to create each little different piece of information its on Reducer, so I am trying to create one Reducer which will handle a lot of very small different pieces of information, and I can't make that work, inside the main "Content" object, I put 2 Key Value Pairs that each have an array, one for each different information, one is "Features" info, and one for the "Header" info.
This is the error that I'm getting:
Uncaught TypeError: Cannot read property 'headerContent' of undefined
at push../src/reducers/ContentReducer.js.__webpack_exports__.default (ContentReducer.js:15)
I am not sure what's the problem, maybe my code is wrong or maybe my use of the spread operator, any solution?
I have added the necessary pages from my code:
ACTIONS FILE
export const addFeatureAction = (
{
title = 'Default feature title',
feature = 'Default feature',
} = {}) => ({
type: 'ADD_FEATURE',
features: {
id: uuid(),
title,
feature
}
})
export const addHeaderAction = (
{
title = 'Default header title',
head = 'Default header',
} = {}) => ({
type: 'ADD_HEADER',
header: {
id: uuid(),
title,
head
}
})
REDUCER FILE:
const defaultContentReducer = {
content: {
featuresContent: [],
headerContent: [],
}
}
export default (state = defaultContentReducer, action) => {
switch(action.type) {
case 'ADD_FEATURE':
return [
...state.content.featuresContent,
action.features
]
case 'ADD_HEADER':
return [
...state.content.headerContent,
action.header
]
default:
return state
}
}
STORE FILE:
export default () => {
const store = createStore(
combineReducers({
articles: ArticleReducer,
qnaList: QnaReducer,
content: ContentReducer
})
);
return store;
}
The reducer function is supposed to return the next state of your application, but you are doing a few things wrong here, you are returning an array, a piece of the state and not the state object, I would suggest you look into immer to prevent this sort of errors.
Simple fix:
export default (state = defaultContentReducer, action) => {
switch(action.type) {
case 'ADD_FEATURE':
return {...state, content: {...state.content. featuresContent: [...action.features, ...state.content.featuresContent]}}
// More actions are handled here
default:
return state
}
}
If you use immer, you should have something like this
export default (state = defaultContentReducer, action) => {
const nextState = produce(state, draftState => {
switch(action.type) {
case 'ADD_FEATURE':
draftState.content.featuresContent = [...draftState.content.featuresContent, ...action.features]
});
break;
default:
break;
return nextState
}