I have a list of users and near each user is a checkbox and a input number.
I want to save all data in the process of changing the data from input and checkbox:
import React, { useState } from "react";
import "./styles.css";
import { InputNumber, Checkbox } from "antd";
export default function App() {
const [user, setUser] = useState([
{
id: "",
nr: "",
checked: false
}
]);
function onChangeNr(id, value) {
console.log("changed", value, id);
setUser([
...user,
{
id: id,
nr: value,
checked: false
}
]);
}
console.log(user);
function onChangeCheck(e, id) {
console.log(`checked = ${e.target.checked}, ${id}`);
setUser([
...user,
{
id: id,
nr: "",
checked: e.target.checked
}
]);
}
return (
<div className="App">
<ul>
{[0, 1, 2, 3, 4].map((i) => {
return (
<li>
<p>User {i}</p>
<InputNumber
min={1}
max={10}
onChange={(id) => onChangeNr(i, id)}
/>
<Checkbox onChange={(check) => onChangeCheck(check, i)}>
Checkbox
</Checkbox>
</li>
);
})}
</ul>
</div>
);
}
Now i dont get the disserved result, but i want to get an array with all objects (unique objects). So each time when user change a data, the last value of the same id should override the previous value and at the final to get something like:
[
{id: 1, nr: 1, checked: true},
{id: 4, nr: 33, checked: true}
]
How to get the above result?
demo: https://codesandbox.io/s/elastic-nobel-79is1?file=/src/App.js:0-1165
To override the old state you can't use spread with an array => That will just combine the two arrays into one.
You either need to implement a search that will loop over all of the objects that you have inside the array and find the one that you need to update :
// loop over all entries
for(const i = 0; i < array.length; i++){
// is not the correct id, so not the correct obejct
if(array[i].id !== id) continue;
// found the correct object
array[i] = {id: i, ...};
}
// update state
setState(array);
But that would make js search every element once someone clicks the up / down arrows, so I would recommend that you put the state of the inputs into the array by their indexies:
// we already know the location of the state for the input because we put it into the same index as the key of the input!
array[i] = {id: i, ...}
setState(array)
Expample CodeSandbox
const reducer = (state, { type, i, checked, number }) => {
switch (type) {
case "setValues":
// reducer needs a new copy of the state
const newState = [...state];
newState[i] = {
id: i,
checked: checked ? checked : state[i]?.checked,
number: number ? number : state[i]?.number
};
console.log(newState);
return newState;
default:
break;
}
};
const [state, dispatch] = useReducer(reducer, initState);
return(
<InputNumber
min={1}
max={10}
onChange={(number) => {
dispatch({ type: "setValues", i, number });
}}
/>)
Related
I'm looking for some help to implement row select functionality in my React app.
I have the following data that I provide to the table(react-data-table-component):
const data = useMemo(
() =>
[...Array(87)].map((_, outer) => ({
id: `o-${outer}`,
name: randFullName(),
age: randNumber({ max: 100 }),
price: randNumber({ max: 5000 }),
description: randProductDescription(),
active: randBoolean(),
date: randFutureDate().toLocaleString(),
items: [...Array(randNumber({ max: 10 }))].map((__, inner) => ({
id: `o-${outer}--i-${inner}`,
name: randFullName(),
age: randNumber({ max: 100 }),
price: randNumber({ max: 5000 }),
description: randProductDescription(),
active: randBoolean(),
date: randFutureDate().toLocaleString(),
})),
})),
[],
);
I display this data in a table.
There is a parent row and each parent row can be expanded which contains the child rows.
Now, I want to be able to select rows. I use a three-state checkbox where three states are:
checked: when all the child rows are checked
empty: when no children row is selected
intermediate: when some of the child rows are selected
I want to render the checkbox states and also be able to access the selected rows.
I am using a custom selector cell like this:
const SelectorCell = ({
rowId,
parentId,
siblingIds,
childrenIds,
}: {
rowId: string;
parentId: string;
siblingIds: string[];
childrenIds: string[];
}) => {
const { toggleRow, rowSelectionStatus } = useContext(
NewTableSelectedRowsContext,
) as NewTableSelectedRowsType;
const handleToggle = useCallback(() => {
// todo
}, []);
const status = rowSelectionStatus(rowId);
return <ThreeStateCheckbox checked={status} onChange={handleToggle} />;
};
I have tried to achieve this using a context provider but haven't got any success:
import { createContext, useCallback, useMemo, useState } from "react";
import {
NewTableSelectedRowsType,
} from "./types";
const statusTypes = {
checked: true,
intermediate: null,
unchecked: false,
};
export const NewTableSelectedRowsContext = createContext<NewTableSelectedRowsType | null>(null);
interface Props {
children: JSX.Element;
}
export const NewTableSelectedRowsContextProvider = ({ children }: Props): JSX.Element => {
const rowSelectionStatus = useCallback((rowId: string) => // todo, []);
const toggleRow = useCallback(() => {
// todo
},
[],
);
const value: NewTableSelectedRowsType = useMemo(
() => ({
toggleRow,
rowSelectionStatus,
}),
[rowSelectionStatus, toggleRow],
);
return (
<NewTableSelectedRowsContext.Provider value={value}>
{children}
</NewTableSelectedRowsContext.Provider>
);
};
How can I achieve that in React?
I'm not sure why you are using context to store the logic of selecting rows or not, you will need to send the data.length to compute if the selected rows are the same amount as the table rows/part of absolutely 0.
You also haven't attached to component code, so my answer will be more general...
You can have state for table status and one for selected rows
enum ETableState {
checked = 'checked',
empty = 'empty',
intermediate = 'intermediate'
}
const [tableState, setTableState] = useState<ETableState>(ETableState.intermediate)
const [selectedRows, setSelectedRows] = useState<string[])([])
on each selecting row, you will call a callback for add/remove it from selectedRows state, that after will trigger an effect
const toggleRow = useCallback((id: string) =>
setSelectedRows(prev =>
prev.includes(id) ? prev.filter(i => i != id) : [...prev, id]
)
, [selectedRows.length])
useEffect(() => {
setTableState(dataLength === selectedRows.length ?
ETableState.checked
: !selectedRows.length ?
ETableState.empty
: ETableState.intermediate
}
,[selectedRows.length, dataLength]
edit
In case you want to be able to use this logic in few separate places, as in child rows and so on, you can wrap all of this code in custom hook and call it in each component with the data.length
function useTableState(dataLength: number) {
...
return { selectedRows, tableState, toggleRow }
}
// usage in a table component
const { selectedRows, tableState, toggleRow } = useTableState(data.length)
I currently have an array of objects. Each array of objects contains a key of checked with a value of type boolean. I am attempting to loop through the array when a user selects a certain checkbox and updating that objects checked value to either true or false. The issue I am having is spreading the updated object back into the array without creating duplicates. My code is as follows:
import { useState } from "react";
import "./styles.css";
export default function App() {
const [arr, setArr] = useState([
{ id: 1, checked: false, name: "Person 1" },
{ id: 2, checked: true, name: "Person 2" }
]);
const updateCheck = (id) => {
const newArr = [...arr];
const object = newArr.find((r) => r.id === id);
const updatedObject = { ...object, checked: !object.checked };
console.log(updatedObject);
};
return (
<div className="App">
{arr.map((r) => {
return (
<>
<label>{r.name}</label>
<input
type="checkbox"
checked={r.checked}
onClick={() => updateCheck(r.id)}
/>
<br />
</>
);
})}
</div>
);
}
The desired effect I would like to achieve is that if Person 1's checkbox gets clicked I update their checked value to the opposite value. So Person 1 would have a checked value of true after their checkbox was clicked.
attached is a code sandbox https://codesandbox.io/s/elegant-haibt-hycmy?file=/src/App.js
Map the array state to create a new array state. If the item being iterated over has an ID that matches, return the updated object from the callback, otherwise return the existing item.
const updateCheck = (id) => setArr(
arr.map(item => (
item.id !== id ? item : { ...item, checked: !item.checked }
))
);
How to add a class name in every row without effect the rest of the rows
import React, { useState } from 'react';
import './testEfect.css';
const Test = () => {
const arrayTest = [
{
name: '11',
id: '11'
},
{
name: '22',
id: '22'
},
{
name: '33',
id: '33'
},
]
const [state, setState] = useState(false);
const handleClick = (event) => {
const newState = event;
setState(state ? false : true);
}
return (
<div className="App">
{arrayTest.map((x, index) => {
return (
<ul key={index} className={state ? 'deletEfect' : ''}>
<li id={x.id} >
{x.name}
<button onClick={(event) => handleClick(x.id)}>Delete</button>
</li>
</ul>
)
})}
</div>
)
}
The problem here is that when you say the state is false; it is assuming the state is false for the whole component. It doesn't update the row but the whole component. So, at first, you need to add a deleted property that will take a different value for each row.
So,
const arrayTest = [
{
name: "11",
id: "11",
deleted: false
},
{
name: "22",
id: "22",
deleted: false
},
{
name: "33",
id: "33",
deleted: false
}
];
const [state, setState] = useState(arrayTest); //initial state
Now, when you render, you don't need to use that arrayTest. But you need to use the state. We won't touch arrayTest ever again. So we use,
{state.map((x, index) => {
return (
<ul key={index} className={x.deleted ? "testEfect" : ""}>
<li id={x.id}>
{x.name}
<button onClick={(event) => handleClick(x.id)}>Delete</button>
</li>
</ul>
);
})}
Notice we use state.map. We also send x.id to handleClick function.
Why? Because we will use that id to change the deleted value of the object. So our handleClick becomes,
const handleClick = (id) => {
const newState = state.map((element) => {
if (element.id === id)
return Object.assign({}, element, {
deleted: element.deleted ? false : true
});
return element;
});
setState(newState);
};
This is just updating the state in an immutable way.
Here is the full codesandbox for your convenience.
Here i have three filters on selection of which i need to filter data in a table.
I am using if else statement to check and filter the data , hence i want to modify the code in some modular way to achieve the same can any one suggest me , should i go with switch case ?
if (mapFilter === 'Mapped') {
if (listFilter) {
const result = fullData.filter(
data =>
data.partner_mapping_classification.length > 0 &&
data.account === listFilter,
);
setFinalData(result);
} else {
const result = fullData.filter(
data => data.partner_mapping_classification.length > 0,
);
setFinalData(result);
}
} else if (mapFilter === 'Not Mapped') {
if (listFilter) {
const result = fullData.filter(
data =>
data.partner_mapping_classification === '' &&
data.account === listFilter,
);
setFinalData(result);
} else {
const result = fullData.filter(
data => data.partner_mapping_classification === '',
);
setFinalData(result);
}
} else if (mapFilter === 'All') {
if (listFilter) {
const result = fullData.filter(
data => data.account === listFilter,
);
setFinalData(result);
} else {
const result = fullData.filter(
data => data.partner_mapping_classification.length > 0,
);
setFinalData(result);
}
} else if (mapFilter === '' && listFilter !== '') {
const result = fullData.filter(
data => data.account === listFilter,
);
setFinalData(result);
} else if (mapFilter === '' && listFilter === '') {
setFinalData([]);
} else {
setFinalData([]);
}
};
Easy to scale method (followed by live-demo)
Using switch statements or multiple chained if( statements (or, even, multiple conditions within same if( statement) doesn't seem to be a good idea, as scaling and maintaining such code will become way too difficult.
As the opposite to above mentioned hardcoding techniques, I would suggest to have an object within your table component's state that will bind object properties (you wish your table entries to get filtered by) to keywords (attached to your inputs).
Assuming (based on your screenshot) you use MaterialUI for styling your components, following example would demonstrate above approach:
const { useState } = React,
{ render } = ReactDOM,
{ Container, TextField, TableContainer, Table, TableHead, TableBody, TableRow, TableCell } = MaterialUI,
rootNode = document.getElementById('root')
const sampleData = [
{id: 0, name: 'apple', category: 'fruit', color: 'green'},
{id: 1, name: 'pear', category: 'fruit', color: 'green'},
{id: 2, name: 'banana', category: 'fruit', color: 'yellow'},
{id: 3, name: 'carrot', category: 'vegie', color: 'red'},
{id: 4, name: 'strawberry', category: 'berry', color: 'red'}
],
sampleColumns = [
{id: 0, property: 'name', columnLabel: 'Item Name'},
{id: 1, property: 'category', columnLabel: 'Category'},
{id: 2, property: 'color', columnLabel: 'Item Color'}
]
const MyFilter = ({filterProperties, onFilter}) => (
<Container>
{
filterProperties.map(({property,id}) => (
<TextField
key={id}
label={property}
name={property}
onKeyUp={onFilter}
/>
))
}
</Container>
)
const MyTable = ({tableData, tableColumns}) => (
<TableContainer>
<Table>
<TableHead>
<TableRow>
{
tableColumns.map(({id, columnLabel}) => (
<TableCell key={id}>
{columnLabel}
</TableCell>
))
}
</TableRow>
</TableHead>
<TableBody>
{
tableData.map(row => (
<TableRow key={row.id}>
{
tableColumns.map(({id, property}) => (
<TableCell key={id}>
{row[property]}
</TableCell>
))
}
</TableRow>
))
}
</TableBody>
</Table>
</TableContainer>
)
const App = () => {
const [state, setState] = useState({
data: sampleData,
columns: sampleColumns,
filterObj: sampleColumns.reduce((r,{property}) => (r[property]='', r), {})
}),
onFilterApply = ({target:{name,value}}) => {
const newFilterObj = {...state.filterObj, [name]: value}
setState({
...state,
filterObj: newFilterObj,
data: sampleData.filter(props =>
Object
.entries(newFilterObj)
.every(([key,val]) =>
!val.length ||
props[key].toLowerCase().includes(val.toLowerCase()))
)
})
}
return (
<Container>
<MyFilter
filterProperties={state.columns}
onFilter={onFilterApply}
/>
<MyTable
tableData={state.data}
tableColumns={state.columns}
/>
</Container>
)
}
render (
<App />,
rootNode
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><script src="https://unpkg.com/#material-ui/core#latest/umd/material-ui.development.js"></script><div id="root"></div>
As Sudhanshu pointed out, you should create event listeners for all these select dropdowns and then update state based on that.
I created a small sample of how I would do it, but just be warned that this isn't tested and I just wrote it without actually running the code or anything. So it is buggy for sure in some regard.
const fullData = ['first', 'second', 'third'];
const BigFilter = () => {
const [activeFilters, setActiveFilters] = useState([]);
const [filteredValues, setFilteredValues] = useState([]);
const handleFilterChange = (event) => {
const { target } = event;
const isInFilter = activeFilters.some((element) => element.name === target.name);
if (!isInFilter) {
setActiveFilters((currentState) => {
return [...currentState, { name: target.name, value: target.value }];
});
} else {
setActiveFilters((currentState) => {
return [...currentState.filter((x) => x.name !== target.name), { name: target.name, value: target.value }];
});
}
};
useEffect(() => {
// Just set full data as filtered values if no filter is active
if (activeFilters.length === 0) {
setFilteredValues([...fullData]);
return;
};
let finalData = [...fullData];
// Returns undefined if it cannot find the element with .name === 'list' in array, otherwise it will return that element
const listData = activeFilters.find((element) => (element.name = 'list'));
if (listData) {
// Do some filtering for first select/dropdown
const { value } = listData;
// value is the value of your select dropdown that was selected
finalData = finalData.filter((x) => x.something > 0);
}
// Returns undefined if it cannot find the element with .name === 'list' in array, otherwise it will return that element
const statusData = activeFilters.find((element) => (element.name = 'status'));
if (statusData) {
// Do some filtering for second select/dropdown
const { value } = statusData;
// value is the value of your select dropdown that was selected
finalData = finalData.filter((x) => x.something > 0);
}
// Returns undefined if it cannot find the element with .name === 'list' in array, otherwise it will return that element
const amountData = activeFilters.find((element) => (element.name = 'amount'));
if (amountData) {
// Do some filtering for third select/dropdown
const { value } = amountData;
// value is the value of your select dropdown that was selected
finalData = finalData.filter((x) => x.something > 0);
}
setFilteredValues(finalData);
// You can go with multiple if statements to filter everything step by step
}, [activeFilters]);
return (
<>
<select name="list" onChange={handleFilterChange}>
<option>List Option 1</option>
</select>
<select name="status" onChange={handleFilterChange}>
<option>Status Option 1</option>
</select>
<select name="amount" onChange={handleFilterChange}>
<option>Amount Option 1</option>
</select>
<div>
{/* Render filtered values */}
{filteredValues.map((singleValue) => singleValue.name)}
</div>
</>
);
};
The basic idea here is that all your <select> elements react to the same event listener, making it easier to coordinate.
You got two basic arrays as state (activeFilters and filteredValues). When onChange handler is triggered, you check the name of the filter and check if that filter is already present in your activeFilters state. If it isn't, you add its name and value to that state. That's why I used name="some name" on each <select> in order to identify it somehow. In other case, if the filter is already present in that state, we remove it and just add its entry again with the new value. (This can probably be written way better, but it's just to give you an idea.)
Both of these cases set new state for active filters with setActiveFilter. Then we have the useEffect hook below which filters all the data based on active filters. As you can see it has that dependency array as a second argument and I added activeFilters variable to it so that every time activeFilters updates it will trigger all the logic in useEffect and it will change your filteredValues.
The logic in useEffect will go step by step and check if each filter is active and filter data for each of them if they are active step by step. If the first filter is active it will filter data that's needed and store it again in finalData and then it will go to the second if statement and if the filter for that is active it will perform another filter, but now on already filtered data. In the end, you should get data that passes through all active filters. I'm sure there's a better way of doing this, but it's a start.
Btw, usually I wouldn't do this
finalData = finalData.filter((x) => x.something > 0);
Re-assigning the same variable with filtered data from it, but I would say it's ok in this case since that finalData variable was created in that useEffect scope and it cannot be mutated from outside the scope. So it's easy to track what it is doing.
I'm sorry if this doesn't work, but it might guide you to your solution.
You can add a filter to the fullData array and provide the value of each of the dropdowns to the filter function
fullData.filter(element => {
return element.account == first && element.account == second && element.account == third;
});
You can also put in checks for the filters, like if the value is just '' then return false i.e return the whole array else return the filtered list
I am doing a project on the React-Redux shopping cart. I am currently trying to build out functionality to allow a user to update the quantity of items being added to the Shopping Cart. I've already been able to get "Remove from Cart" to work. I've been searching around for different ways of doing it, but it seems that all of the Shopping Cart tutorials stop at "Add to Cart"! So I've been trying to puzzle through it on my own, but found very few examples online. Can anyone point me in the right direction?
Here's the shopping cart tutorial originally posted on Github:
https://github.com/reactjs/redux/tree/master/examples/shopping-cart
Here's what I've been trying to figure out:
ProductItem.js
const ProductItem = ({product, onAddToCartClicked, onRemoveFromCartClicked, onIncreaseQuanityClicked, onDecreaseQuantityClicked }) => (
<div style={{ marginBottom: 20, marginLeft: 20}}>
<Card>
<CardBody>
<Product
title={product.title}
price={product.price}
inventory={product.inventory} />
<Button color="primary"
onClick={onAddToCartClicked}
disabled={product.inventory > 0 ? '' : 'disabled'}>
{product.inventory > 0 ? 'Add to cart' : 'Sold Out'}
</Button>
<Button color="success"
onClick={onIncreaseQuanityClicked}
disabled={product.inventory > 0 ? '' : 'disabled'}> +
</Button>
<Button color="danger"
onclick={onDecreaseQuantityClicked}
disabled={product.inventory > 0 ? '' : 'disabled'}> -
</Button>
<Button onClick={onRemoveFromCartClicked}>Remove</Button>
</CardBody>
</Card>
</div>
)
ProductsContainer.js
const ProductsContainer = ({ products, addToCart }) => (
<ProductsList title="Products">
{products.map(product =>
<ProductItem
key={product.id}
product={product}
onAddToCartClicked={() => addToCart(product.id)}
onIncreaseQuantityClicked={() => increaseQuantity(product.id)}
onDecreaseQuantityClicked={() => decreaseQuantity(product.id)} />
)}
</ProductsList>
)
ProductsContainer.propTypes = {
products: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
inventory: PropTypes.number.isRequired
})).isRequired,
addToCart: PropTypes.func.isRequired,
increaseQuantity: PropTypes.func.isRequired
}
const mapStateToProps = state => ({
products: getVisibleProducts(state.products)
})
export default connect(
mapStateToProps,
{ addToCart, increaseQuantity, decreaseQuantity }
)(ProductsContainer)
reducer/products.js
const products = (state, action) => {
switch (action.type) {
case ADD_TO_CART:
return {
...state,
inventory: state.inventory - 1
}
case REMOVE_FROM_CART:
return {
...state,
inventory: state.inventory + 1
}
case INCREASE_QUANTITY:
return {
...state,
//NOT SURE WHAT ELSE TO PUT HERE
}
case DECREASE_QUANTITY:
return {
...state,
NOT SURE WHAT ELSE TO PUT HERE EITHER
}
default:
return state
}
}
Can anyone point me in the right path? If I'm even on the right path at all, or suggest any tutorials or websites that could help?
First, I think you should include quantity in your state and separate the logic of quantity from inventory. Here's what your state tree can look like:
{
cart: [
{id: 1, quantity: 3},
{id: 3, quantity: 2}
],
products: [
{id: 1, inventory: 10, ...},
{id: 2, inventory: 10, ...},
{id: 3, inventory: 10, ...}
]
}
Cart stores the products added to the cart and products contains all of the available products.
With this state tree in mind, we can use the following action creators:
function quantityUp(id, val){
return {type: 'QTY_UP', id, up: val}
}
function quantityDown(id, val){
return {type: 'QTY_DOWN', id, down: val}
}
Now, we can create our reducers. Since we separated quantity from inventory, we should also separate the reducers to reflect this logic.
const cart = (state, action) => {
switch(action.type){
case 'QTY_UP':
return Object.assign([], state.map(item => {
if(item.id === action.id){
item.quantity += action.up;
}
return item;
));
case 'QTY_DOWN':
return Object.assign([], state.map(item => {
if(item.id === action.id){
item.quantity -= action.down;
}
return item;
));
default:
return state;
}
};
The following actions should also be part of your cart reducer: ADD_TO_CART, REMOVE_FROM_CART
The products reducer should take care of modifying the products themselves, if needed. One case would be to modify the inventory of an item when an item has been purchased.
Let's create the action creators first:
//cart will be an array
function purchase(cart){
return {type: 'PURCHASE', cart}
}
Now we can create the reducer:
const products = (state, action) => {
switch(action.type){
case 'PURCHASE':
const ids = action.cart.map(item => item.id);
return Object.assign([], state.map(item => {
if(ids.includes(item.id)){
item.inventory -= action.cart.filter(p => p.id === item.id)[0].quantity;
}
return item;
}));
case default:
return state;
}
};
Now we can add products to your cart, edit the quantities of each product in the cart, and update the inventory of each product in your state when a product has been purchased.
Be sure to have an initial state setup before your reducer like:
const initialState = {
inventory: 0,
quantity: 0
}
Then you link your reducer to the state that you just declared:
const products = (state = initialState, action) => {
if you want your action to increase your quantity in state, you proceed as with inventory:
quantity: state.quantity + 1
As a reminder, the state is first initiated through one or multiple reducers and you create a store in redux by using for example
const store = createStore(yourReducer)
or
const store = createStore(combineReducers(allYourReducers))
Your store will have the global state of your app made of the sum of all your reducer's initialStates.
Then you can access and play with the state by dispatching your actions
store.dispatch(yourAction)
If everything in your app is well connected, you should see your state updating as you want.
You may check this course from Andrew Mead : https://www.udemy.com/react-2nd-edition/learn/v4/overview
Given your current code, addToCart and increaseQuantity are the same thing.
You can either:
1) Reuse the addToCart function in your container
<ProductItem
key={product.id}
product={product}
onAddToCartClicked={() => addToCart(product.id)}
onIncreaseQuantityClicked={() => addToCart(product.id)}
onDecreaseQuantityClicked={() => decreaseQuantity(product.id)} />
2) Implement the same logic in your reducer
case INCREASE_QUANTITY:
return {
...state,
inventory: state.inventory - 1
}
when we work on shopping cart, we should have cartItems array inside our cart state and every time we click on "add to cart" button, that item will be pushed to that array and we will "map" that array in the component we wanna render cart items.
const INITIAL_STATE = {
//you could have more properties but i focus on cartItems
cartItems: []
};
to add item to the cart, we should be careful when we write our code. because first time adding an item to the cart is easy, but what if we add the same item multiple times to the cart. so we need to group items inside the cartItems array. for this we need to write an utility function.
//cart.utils.js
export const addItemToCart = (cartItems, cartItemToAdd) => {
//find(condition) finds the first item in the array based on the condition.
const existingCartItem = cartItems.find(item => item.id === cartItemToAdd.id);
if (existingCartItem) {
//in order for change detection to trigger we have to rerender
//otherwise our quantity property will not be updated
//map will return a new array
//we need to return new versions of our state so that our component know to re render
//here we update the quantity property
return cartItems.map(item =>
item.id === cartItemToAdd.id
? { ...cartItemToAdd, quantity: item.quantity + 1 }
: item
);
}
//when you first time add a new item, sine exixtingCartItem will be falsy, it will pass the first if block and will come here
//quantity property gets attached the first time around since this if block wont run when it is a new item.
//in the beginning cartItems array is empty. every time you add a new item to this array, it will add "quantity:1" to this item object.
return [...cartItems, { ...cartItemToAdd, quantity: 1 }];
};
in your reducer file
import { addItemToCart } from "./cart.utils";
case INCREASE_QUANTITY:
return {
...state,
cartItems: addItemToCart(state.cartItems, action.payload)
};
for removing item from the cart, we need to write another utility function.
cart.utils.js
export const removeItemFromCart = (cartItems, cartItemToRemove) => {
//check if item is already in the cartItems
const existingCartItem = cartItems.find(
item => item.id === cartItemToRemove.id
);
//if there is only 1, upon clicking, we should remove the item from the array
if (existingCartItem.quantity === 1) {
return cartItems.filter(item => item.id !== cartItemToRemove.id);
}
return cartItems.map(item =>
item.id === cartItemToRemove.id
? { ...item, quantity: item.quantity - 1 }
: item
);
};
in reducer/products.js
import { addItemToCart, removeItemFromCart } from "./cart.utils";
case DECREASE_QUANTITY:
return {
...state,
cartItems: removeItemFromCart(state.cartItems, action.payload)
}